From 28b517da40a579454b727797b1435e9287c510d9 Mon Sep 17 00:00:00 2001 From: liu <362165265@qq.com> Date: Tue, 3 Feb 2026 23:24:32 +0800 Subject: [PATCH] chore: initial project setup --- .gitignore | 46 ++ 01-技术选型.md | 129 ++++ README.md | 94 +++ init-git.bat | 29 + init-git.ps1 | 52 ++ pom.xml | 116 ++++ src/main/java/com/svnmanager/MainApp.java | 48 ++ .../svnmanager/controller/MainController.java | 559 ++++++++++++++++++ .../controller/ProjectDialogController.java | 79 +++ .../java/com/svnmanager/model/Project.java | 117 ++++ .../com/svnmanager/model/SvnFileStatus.java | 91 +++ .../java/com/svnmanager/model/SvnInfo.java | 100 ++++ .../java/com/svnmanager/model/SvnLog.java | 72 +++ .../java/com/svnmanager/model/SvnStatus.java | 95 +++ .../svnmanager/service/CheckoutService.java | 67 +++ .../com/svnmanager/service/CommitService.java | 134 +++++ .../com/svnmanager/service/DiffService.java | 64 ++ .../com/svnmanager/service/InfoService.java | 84 +++ .../com/svnmanager/service/LogService.java | 139 +++++ .../com/svnmanager/service/StatusService.java | 102 ++++ .../com/svnmanager/service/SvnService.java | 64 ++ .../com/svnmanager/service/UpdateService.java | 126 ++++ .../java/com/svnmanager/util/ConfigUtil.java | 157 +++++ .../java/com/svnmanager/util/LogUtil.java | 51 ++ .../java/com/svnmanager/util/ProcessUtil.java | 159 +++++ src/main/resources/application.properties | 11 + src/main/resources/css/styles.css | 377 ++++++++++++ src/main/resources/fxml/main.fxml | 122 ++++ src/main/resources/fxml/project-dialog.fxml | 47 ++ src/main/resources/logback.xml | 26 + src/main/resources/projects.json | 1 + ui-preview.html | 418 +++++++++++++ 32 files changed, 3776 insertions(+) create mode 100644 .gitignore create mode 100644 01-技术选型.md create mode 100644 README.md create mode 100644 init-git.bat create mode 100644 init-git.ps1 create mode 100644 pom.xml create mode 100644 src/main/java/com/svnmanager/MainApp.java create mode 100644 src/main/java/com/svnmanager/controller/MainController.java create mode 100644 src/main/java/com/svnmanager/controller/ProjectDialogController.java create mode 100644 src/main/java/com/svnmanager/model/Project.java create mode 100644 src/main/java/com/svnmanager/model/SvnFileStatus.java create mode 100644 src/main/java/com/svnmanager/model/SvnInfo.java create mode 100644 src/main/java/com/svnmanager/model/SvnLog.java create mode 100644 src/main/java/com/svnmanager/model/SvnStatus.java create mode 100644 src/main/java/com/svnmanager/service/CheckoutService.java create mode 100644 src/main/java/com/svnmanager/service/CommitService.java create mode 100644 src/main/java/com/svnmanager/service/DiffService.java create mode 100644 src/main/java/com/svnmanager/service/InfoService.java create mode 100644 src/main/java/com/svnmanager/service/LogService.java create mode 100644 src/main/java/com/svnmanager/service/StatusService.java create mode 100644 src/main/java/com/svnmanager/service/SvnService.java create mode 100644 src/main/java/com/svnmanager/service/UpdateService.java create mode 100644 src/main/java/com/svnmanager/util/ConfigUtil.java create mode 100644 src/main/java/com/svnmanager/util/LogUtil.java create mode 100644 src/main/java/com/svnmanager/util/ProcessUtil.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/css/styles.css create mode 100644 src/main/resources/fxml/main.fxml create mode 100644 src/main/resources/fxml/project-dialog.fxml create mode 100644 src/main/resources/logback.xml create mode 100644 src/main/resources/projects.json create mode 100644 ui-preview.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..892e803 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# IDE +.idea/ +*.iml +*.iws +*.ipr +.vscode/ +.settings/ +.classpath +.project +.factorypath + +# OS +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# Logs +*.log +logs/ + +# Java +*.class +*.jar +*.war +*.ear +*.nar +hs_err_pid* + +# Temporary files +*.tmp +*.bak +*.cache diff --git a/01-技术选型.md b/01-技术选型.md new file mode 100644 index 0000000..f73d1c3 --- /dev/null +++ b/01-技术选型.md @@ -0,0 +1,129 @@ +# 01-技术选型 + +## 推荐技术栈 + +### 1. UI框架 +**选择:JavaFX** + +**理由:** +- 现代化UI组件,支持CSS样式定制 +- 响应式布局,适合构建复杂的SVN管理界面 +- 官方Scene Builder可视化设计工具,开发效率高 +- 跨平台性好 + +**对比Swing:** +- Swing虽然成熟,但UI组件较为陈旧 +- JavaFX提供更丰富的动画和视觉效果 +- JavaFX更适合现代桌面应用开发 + +### 2. 构建工具 +**选择:Maven** + +**理由:** +- 生态成熟,依赖管理简单 +- 项目结构标准化 +- 插件丰富 +- 社区支持好 + +### 3. 进程执行 +**选择:ProcessBuilder** + +**理由:** +- Java原生,无需额外依赖 +- 足够应对SVN命令调用需求 +- 支持环境变量配置和工作目录设置 + +**可选增强:** +- Apache Commons Exec(提供更高级的进程管理功能) + +### 4. Java版本 +**选择:Java 11+** + +**理由:** +- LTS(长期支持)版本,稳定可靠 +- JavaFX支持良好 +- 性能优化完善 + +### 5. 辅助库(可选) +- **Jackson/Gson**: JSON解析,解析SVN的XML/JSON输出格式 +- **Logback/SLF4J**: 日志管理 +- **JUnit 5**: 单元测试 + +## 架构设计 + +### MVC模式 +``` +├── Controller/ # 处理UI交互 +│ ├── MainController.java +│ ├── CheckoutController.java +│ ├── UpdateController.java +│ └── CommitController.java +├── Service/ # 封装SVN命令 +│ ├── SvnService.java +│ ├── CheckoutService.java +│ ├── UpdateService.java +│ ├── CommitService.java +│ └── StatusService.java +├── Model/ # SVN输出解析器 +│ ├── SvnStatus.java +│ ├── SvnLog.java +│ └── SvnInfo.java +└── Utils/ # 工具类 + ├── ProcessUtil.java + ├── LogUtil.java + └── ConfigUtil.java +``` + +## 项目结构 +``` +svn-manager/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/svnmanager/ +│ │ │ ├── controller/ +│ │ │ ├── service/ +│ │ │ ├── model/ +│ │ │ ├── util/ +│ │ │ └── MainApp.java +│ │ └── resources/ +│ │ ├── fxml/ +│ │ ├── css/ +│ │ └── application.properties +│ └── test/ +│ └── java/ +├── pom.xml +└── docs/ + └── 01-技术选型.md +``` + +## 核心功能模块 + +### 1. 仓库管理 +- Checkout检出仓库 +- Update更新仓库 +- Commit提交修改 + +### 2. 文件操作 +- Add添加文件 +- Delete删除文件 +- Revert回退文件 + +### 3. 版本查看 +- Status查看状态 +- Log查看日志 +- Diff查看差异 +- Info查看信息 + +### 4. 分支管理 +- Create Branch创建分支 +- Switch切换分支 +- Merge合并分支 + +## 技术优势总结 + +1. **轻量级**: 基于原生SVN命令,无需复杂的SVN客户端库 +2. **跨平台**: JavaFX + Java 11实现一次编写,多处运行 +3. **易维护**: 标准的Maven项目结构,清晰的代码组织 +4. **可扩展**: MVC架构便于功能扩展和维护 +5. **用户友好**: JavaFX提供现代化的用户体验 diff --git a/README.md b/README.md new file mode 100644 index 0000000..628792c --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# SVN Manager + +SVN管理工具 - 多项目管理界面 + +## 项目简介 + +基于 JavaFX 开发的 SVN 管理工具,提供图形化界面进行 SVN 仓库的日常操作。 + +## 技术栈 + +- **Java**: 11+ +- **JavaFX**: 17.0.2 +- **Maven**: 构建工具 +- **Jackson**: JSON 解析 +- **Logback**: 日志管理 + +## 功能特性 + +### 1. 仓库管理 +- Checkout 检出仓库 +- Update 更新仓库 +- Commit 提交修改 + +### 2. 文件操作 +- Add 添加文件 +- Delete 删除文件 +- Revert 回退文件 + +### 3. 版本查看 +- Status 查看状态 +- Log 查看日志 +- Diff 查看差异 +- Info 查看信息 + +### 4. 分支管理 +- Create Branch 创建分支 +- Switch 切换分支 +- Merge 合并分支 + +## 项目结构 + +``` +svn-manager/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/svnmanager/ +│ │ │ ├── controller/ # UI控制器 +│ │ │ ├── service/ # SVN服务封装 +│ │ │ ├── model/ # 数据模型 +│ │ │ ├── util/ # 工具类 +│ │ │ └── MainApp.java +│ │ └── resources/ +│ │ ├── fxml/ # FXML界面文件 +│ │ ├── css/ # 样式文件 +│ │ └── application.properties +│ └── test/ +├── pom.xml +└── README.md +``` + +## 构建与运行 + +### 前置要求 +- JDK 11 或更高版本 +- Maven 3.6+ +- SVN 客户端已安装并配置在系统 PATH 中 + +### 编译项目 +```bash +mvn clean compile +``` + +### 运行项目 +```bash +mvn javafx:run +``` + +### 打包项目 +```bash +mvn clean package +``` + +## 开发说明 + +项目采用 MVC 架构模式: +- **Controller**: 处理 UI 交互逻辑 +- **Service**: 封装 SVN 命令调用 +- **Model**: 解析 SVN 输出数据 +- **Util**: 提供通用工具方法 + +## 许可证 + +MIT License diff --git a/init-git.bat b/init-git.bat new file mode 100644 index 0000000..8916482 --- /dev/null +++ b/init-git.bat @@ -0,0 +1,29 @@ +@echo off +chcp 65001 >nul +echo 开始初始化 Git 仓库... + +if exist .git ( + echo Git 仓库已存在,跳过初始化 +) else ( + git init + echo Git 仓库初始化完成 +) + +echo 添加文件到暂存区... +git add . + +echo 提交更改... +git commit -m "chore: initial project setup" + +echo 添加远程仓库... +git remote remove origin 2>nul +git remote add origin git@gitee.com:liujingaiyuanjiao/svn-manager.git + +echo 推送到 Gitee... +git branch -M master 2>nul +git push -u origin master + +echo. +echo 完成! +echo 仓库地址: https://gitee.com/liujingaiyuanjiao/svn-manager +pause diff --git a/init-git.ps1 b/init-git.ps1 new file mode 100644 index 0000000..a528dfa --- /dev/null +++ b/init-git.ps1 @@ -0,0 +1,52 @@ +# Git 初始化并推送到 Gitee 脚本 +# 使用方法:在 PowerShell 中执行 .\init-git.ps1 + +Write-Host "开始初始化 Git 仓库..." -ForegroundColor Green + +# 检查是否已初始化 +if (Test-Path .git) { + Write-Host "Git 仓库已存在,跳过初始化" -ForegroundColor Yellow +} else { + git init + Write-Host "Git 仓库初始化完成" -ForegroundColor Green +} + +# 添加所有文件 +Write-Host "添加文件到暂存区..." -ForegroundColor Green +git add . + +# 检查是否有未提交的更改 +$status = git status --porcelain +if ($status) { + # 提交更改 + Write-Host "提交更改..." -ForegroundColor Green + git commit -m "chore: initial project setup" + Write-Host "提交完成" -ForegroundColor Green +} else { + Write-Host "没有需要提交的更改" -ForegroundColor Yellow +} + +# 检查远程仓库是否已配置 +$remote = git remote get-url origin 2>$null +if ($LASTEXITCODE -ne 0) { + # 添加远程仓库 + Write-Host "添加远程仓库..." -ForegroundColor Green + git remote add origin git@gitee.com:liujingaiyuanjiao/svn-manager.git + Write-Host "远程仓库已添加" -ForegroundColor Green +} else { + Write-Host "远程仓库已配置: $remote" -ForegroundColor Yellow +} + +# 获取当前分支名 +$branch = git branch --show-current +if (-not $branch) { + $branch = "master" + git branch -M master +} + +Write-Host "推送到 Gitee..." -ForegroundColor Green +Write-Host "分支: $branch" -ForegroundColor Cyan +git push -u origin $branch + +Write-Host "完成!" -ForegroundColor Green +Write-Host "仓库地址: https://gitee.com/liujingaiyuanjiao/svn-manager" -ForegroundColor Cyan diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ac86305 --- /dev/null +++ b/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + com.svnmanager + svn-manager + 1.0.0 + jar + + SVN Manager + SVN管理工具 - 多项目管理界面 + + + UTF-8 + 11 + 11 + 17.0.2 + 2.15.2 + 1.4.8 + 2.0.7 + 5.10.0 + + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 11 + 11 + UTF-8 + + + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + com.svnmanager.MainApp + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + + diff --git a/src/main/java/com/svnmanager/MainApp.java b/src/main/java/com/svnmanager/MainApp.java new file mode 100644 index 0000000..0a95e4a --- /dev/null +++ b/src/main/java/com/svnmanager/MainApp.java @@ -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); + } +} diff --git a/src/main/java/com/svnmanager/controller/MainController.java b/src/main/java/com/svnmanager/controller/MainController.java new file mode 100644 index 0000000..0d6eed0 --- /dev/null +++ b/src/main/java/com/svnmanager/controller/MainController.java @@ -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 fileStatusTable; + @FXML private TableColumn statusColumn; + @FXML private TableColumn pathColumn; + @FXML private TableColumn actionColumn; + + private Project currentProject; + private ObservableList fileStatusList; + private ObservableList 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() { + 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 task = new Task() { + @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 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 task = new Task() { + @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 task = new Task() { + @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 task = new Task() { + @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> task = new Task>() { + @Override + protected List call() throws Exception { + return logService.getLog(currentProject.getPath(), 20); + } + }; + + task.setOnSucceeded(e -> { + List 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 task = new Task() { + @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 task = new Task() { + @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 logs) { + Dialog 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 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 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(); + } +} diff --git a/src/main/java/com/svnmanager/controller/ProjectDialogController.java b/src/main/java/com/svnmanager/controller/ProjectDialogController.java new file mode 100644 index 0000000..f669c26 --- /dev/null +++ b/src/main/java/com/svnmanager/controller/ProjectDialogController.java @@ -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()); + } + } +} diff --git a/src/main/java/com/svnmanager/model/Project.java b/src/main/java/com/svnmanager/model/Project.java new file mode 100644 index 0000000..42123e2 --- /dev/null +++ b/src/main/java/com/svnmanager/model/Project.java @@ -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; + } + } +} diff --git a/src/main/java/com/svnmanager/model/SvnFileStatus.java b/src/main/java/com/svnmanager/model/SvnFileStatus.java new file mode 100644 index 0000000..aee4699 --- /dev/null +++ b/src/main/java/com/svnmanager/model/SvnFileStatus.java @@ -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; + } + } +} diff --git a/src/main/java/com/svnmanager/model/SvnInfo.java b/src/main/java/com/svnmanager/model/SvnInfo.java new file mode 100644 index 0000000..bac1450 --- /dev/null +++ b/src/main/java/com/svnmanager/model/SvnInfo.java @@ -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; + } +} diff --git a/src/main/java/com/svnmanager/model/SvnLog.java b/src/main/java/com/svnmanager/model/SvnLog.java new file mode 100644 index 0000000..ed98902 --- /dev/null +++ b/src/main/java/com/svnmanager/model/SvnLog.java @@ -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 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 getChangedPaths() { + return changedPaths; + } + + public void setChangedPaths(List changedPaths) { + this.changedPaths = changedPaths; + } + + public void addChangedPath(String path) { + this.changedPaths.add(path); + } +} diff --git a/src/main/java/com/svnmanager/model/SvnStatus.java b/src/main/java/com/svnmanager/model/SvnStatus.java new file mode 100644 index 0000000..9f8c13b --- /dev/null +++ b/src/main/java/com/svnmanager/model/SvnStatus.java @@ -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 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 getFiles() { + return files; + } + + public void setFiles(List 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; + } + } + } +} diff --git a/src/main/java/com/svnmanager/service/CheckoutService.java b/src/main/java/com/svnmanager/service/CheckoutService.java new file mode 100644 index 0000000..80f4e60 --- /dev/null +++ b/src/main/java/com/svnmanager/service/CheckoutService.java @@ -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 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); + } +} diff --git a/src/main/java/com/svnmanager/service/CommitService.java b/src/main/java/com/svnmanager/service/CommitService.java new file mode 100644 index 0000000..d265b84 --- /dev/null +++ b/src/main/java/com/svnmanager/service/CommitService.java @@ -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 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 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; + } + } +} diff --git a/src/main/java/com/svnmanager/service/DiffService.java b/src/main/java/com/svnmanager/service/DiffService.java new file mode 100644 index 0000000..bc82736 --- /dev/null +++ b/src/main/java/com/svnmanager/service/DiffService.java @@ -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 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); + } +} diff --git a/src/main/java/com/svnmanager/service/InfoService.java b/src/main/java/com/svnmanager/service/InfoService.java new file mode 100644 index 0000000..b6b4ff2 --- /dev/null +++ b/src/main/java/com/svnmanager/service/InfoService.java @@ -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()); + } + } + } +} diff --git a/src/main/java/com/svnmanager/service/LogService.java b/src/main/java/com/svnmanager/service/LogService.java new file mode 100644 index 0000000..0d139f8 --- /dev/null +++ b/src/main/java/com/svnmanager/service/LogService.java @@ -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 getLog(String workingDirectory, Integer limit) + throws IOException, InterruptedException, TimeoutException { + logger.debug("获取日志: {}", workingDirectory); + + if (!isValidWorkingCopy(workingDirectory)) { + throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory); + } + + List 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 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 getLog(String workingDirectory) + throws IOException, InterruptedException, TimeoutException { + return getLog(workingDirectory, 50); + } + + /** + * 解析svn log输出 + * + * @param output 命令输出 + * @param logs 日志列表 + */ + private void parseLogOutput(String output, List 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); + } + } +} diff --git a/src/main/java/com/svnmanager/service/StatusService.java b/src/main/java/com/svnmanager/service/StatusService.java new file mode 100644 index 0000000..94ddd2a --- /dev/null +++ b/src/main/java/com/svnmanager/service/StatusService.java @@ -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 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 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); + } +} diff --git a/src/main/java/com/svnmanager/service/SvnService.java b/src/main/java/com/svnmanager/service/SvnService.java new file mode 100644 index 0000000..e74a7b4 --- /dev/null +++ b/src/main/java/com/svnmanager/service/SvnService.java @@ -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 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; + } + } +} diff --git a/src/main/java/com/svnmanager/service/UpdateService.java b/src/main/java/com/svnmanager/service/UpdateService.java new file mode 100644 index 0000000..48cffe2 --- /dev/null +++ b/src/main/java/com/svnmanager/service/UpdateService.java @@ -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 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; + } + } +} diff --git a/src/main/java/com/svnmanager/util/ConfigUtil.java b/src/main/java/com/svnmanager/util/ConfigUtil.java new file mode 100644 index 0000000..63fe2a5 --- /dev/null +++ b/src/main/java/com/svnmanager/util/ConfigUtil.java @@ -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 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 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 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 projects = loadProjects(); + projects.add(project); + return saveProjects(projects); + } + + /** + * 更新项目 + * + * @param project 项目 + * @return 是否更新成功 + */ + public static boolean updateProject(Project project) { + List 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 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 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; + } +} diff --git a/src/main/java/com/svnmanager/util/LogUtil.java b/src/main/java/com/svnmanager/util/LogUtil.java new file mode 100644 index 0000000..4c3e8e4 --- /dev/null +++ b/src/main/java/com/svnmanager/util/LogUtil.java @@ -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); + } +} diff --git a/src/main/java/com/svnmanager/util/ProcessUtil.java b/src/main/java/com/svnmanager/util/ProcessUtil.java new file mode 100644 index 0000000..879a5d1 --- /dev/null +++ b/src/main/java/com/svnmanager/util/ProcessUtil.java @@ -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 outputLines = new ArrayList<>(); + List 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 args, String workingDirectory) + throws IOException, InterruptedException, TimeoutException { + List 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 output; + private final List error; + + public ProcessResult(int exitCode, List output, List error) { + this.exitCode = exitCode; + this.output = output; + this.error = error; + } + + public int getExitCode() { + return exitCode; + } + + public List getOutput() { + return output; + } + + public List 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; + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..b2d3088 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,11 @@ +# SVN Manager Application Configuration +app.name=SVN管理器 +app.version=1.0.0 + +# SVN Command Configuration +svn.command=svn +svn.timeout=300 + +# UI Configuration +ui.theme=default +ui.language=zh_CN diff --git a/src/main/resources/css/styles.css b/src/main/resources/css/styles.css new file mode 100644 index 0000000..5a2edbd --- /dev/null +++ b/src/main/resources/css/styles.css @@ -0,0 +1,377 @@ +/* 全局样式 */ +.root { + -fx-font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; + -fx-font-size: 14px; +} + +/* 左侧边栏 */ +.sidebar { + -fx-background-color: white; + -fx-border-color: #e2e8f0; + -fx-border-width: 0 1 0 0; +} + +.sidebar-header { + -fx-background-color: white; + -fx-border-color: #e2e8f0; + -fx-border-width: 0 0 1 0; +} + +.logo-icon { + -fx-background-color: linear-gradient(to bottom right, #6366f1, #8b5cf6); + -fx-background-radius: 8; +} + +.app-title { + -fx-font-size: 20px; + -fx-font-weight: bold; + -fx-text-fill: #111827; +} + +.app-subtitle { + -fx-font-size: 12px; + -fx-text-fill: #6b7280; +} + +.add-project-button { + -fx-background-color: #4f46e5; + -fx-text-fill: white; + -fx-font-weight: 500; + -fx-background-radius: 8; + -fx-padding: 10 16; + -fx-cursor: hand; +} + +.add-project-button:hover { + -fx-background-color: #4338ca; +} + +.project-list { + -fx-background-color: white; +} + +.project-card { + -fx-background-color: #f9fafb; + -fx-background-radius: 12; + -fx-padding: 16; + -fx-cursor: hand; +} + +.project-card:hover { + -fx-background-color: #f3f4f6; + -fx-translate-x: 4; +} + +.project-card.active { + -fx-background-color: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.project-card.active .project-name { + -fx-text-fill: white; +} + +.project-card.active .project-path { + -fx-text-fill: rgba(255, 255, 255, 0.8); +} + +.project-icon { + -fx-background-color: #e0e7ff; + -fx-background-radius: 8; + -fx-pref-width: 48; + -fx-pref-height: 48; +} + +.project-card.active .project-icon { + -fx-background-color: rgba(255, 255, 255, 0.2); +} + +.project-name { + -fx-font-size: 14px; + -fx-font-weight: 600; + -fx-text-fill: #111827; +} + +.project-path { + -fx-font-size: 12px; + -fx-text-fill: #6b7280; +} + +.status-badge { + -fx-background-color: #d1fae5; + -fx-background-radius: 12; + -fx-padding: 4 8; +} + +.status-badge.synced { + -fx-background-color: #d1fae5; +} + +.status-badge.updates { + -fx-background-color: #fef3c7; +} + +.status-badge.disconnected { + -fx-background-color: #f3f4f6; +} + +.status-dot { + -fx-background-color: #10b981; + -fx-background-radius: 4; + -fx-pref-width: 8; + -fx-pref-height: 8; +} + +.status-dot.updates { + -fx-background-color: #f59e0b; +} + +.status-dot.disconnected { + -fx-background-color: #9ca3af; +} + +.sidebar-footer { + -fx-background-color: white; + -fx-border-color: #e2e8f0; + -fx-border-width: 1 0 0 0; +} + +.project-count { + -fx-font-size: 12px; + -fx-text-fill: #6b7280; + -fx-alignment: center; +} + +/* 工具栏 */ +.toolbar { + -fx-background-color: white; + -fx-border-color: #e2e8f0; + -fx-border-width: 0 0 1 0; +} + +.project-title-label { + -fx-font-size: 24px; + -fx-font-weight: bold; + -fx-text-fill: #111827; +} + +.project-path-label { + -fx-font-size: 14px; + -fx-text-fill: #6b7280; +} + +.refresh-button { + -fx-background-color: #f3f4f6; + -fx-text-fill: #374151; + -fx-font-weight: 500; + -fx-background-radius: 8; + -fx-padding: 8 16; + -fx-cursor: hand; +} + +.refresh-button:hover { + -fx-background-color: #e5e7eb; +} + +.execute-button { + -fx-background-color: #4f46e5; + -fx-text-fill: white; + -fx-font-weight: 500; + -fx-background-radius: 8; + -fx-padding: 8 16; + -fx-cursor: hand; +} + +.execute-button:hover { + -fx-background-color: #4338ca; +} + +/* 操作按钮组 */ +.action-buttons { + -fx-background-color: white; + -fx-border-color: #e2e8f0; + -fx-border-width: 0 0 1 0; +} + +.action-btn { + -fx-font-weight: 500; + -fx-background-radius: 8; + -fx-padding: 8 16; + -fx-cursor: hand; + -fx-border-width: 1; + -fx-border-radius: 8; +} + +.checkout-btn { + -fx-background-color: #dbeafe; + -fx-text-fill: #1e40af; + -fx-border-color: #bfdbfe; +} + +.checkout-btn:hover { + -fx-background-color: #bfdbfe; + -fx-translate-y: -2; +} + +.update-btn { + -fx-background-color: #d1fae5; + -fx-text-fill: #065f46; + -fx-border-color: #a7f3d0; +} + +.update-btn:hover { + -fx-background-color: #a7f3d0; + -fx-translate-y: -2; +} + +.commit-btn { + -fx-background-color: #f3e8ff; + -fx-text-fill: #6b21a8; + -fx-border-color: #e9d5ff; +} + +.commit-btn:hover { + -fx-background-color: #e9d5ff; + -fx-translate-y: -2; +} + +.status-btn { + -fx-background-color: #fef3c7; + -fx-text-fill: #92400e; + -fx-border-color: #fde68a; +} + +.status-btn:hover { + -fx-background-color: #fde68a; + -fx-translate-y: -2; +} + +.log-btn { + -fx-background-color: #e0e7ff; + -fx-text-fill: #3730a3; + -fx-border-color: #c7d2fe; +} + +.log-btn:hover { + -fx-background-color: #c7d2fe; + -fx-translate-y: -2; +} + +.diff-btn { + -fx-background-color: #f3f4f6; + -fx-text-fill: #374151; + -fx-border-color: #e5e7eb; +} + +.diff-btn:hover { + -fx-background-color: #e5e7eb; + -fx-translate-y: -2; +} + +.info-btn { + -fx-background-color: #f3f4f6; + -fx-text-fill: #374151; + -fx-border-color: #e5e7eb; +} + +.info-btn:hover { + -fx-background-color: #e5e7eb; + -fx-translate-y: -2; +} + +/* 统计卡片 */ +.stat-card { + -fx-background-color: white; + -fx-background-radius: 12; + -fx-border-color: #e5e7eb; + -fx-border-radius: 12; + -fx-padding: 20; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.05), 4, 0, 0, 2); +} + +.stat-label { + -fx-font-size: 14px; + -fx-text-fill: #6b7280; +} + +.stat-value { + -fx-font-size: 24px; + -fx-font-weight: bold; + -fx-text-fill: #111827; +} + +.stat-description { + -fx-font-size: 12px; + -fx-text-fill: #6b7280; +} + +/* 文件状态卡片 */ +.file-status-card { + -fx-background-color: white; + -fx-background-radius: 12; + -fx-border-color: #e5e7eb; + -fx-border-radius: 12; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.05), 4, 0, 0, 2); +} + +.file-status-header { + -fx-background-color: #f8fafc; + -fx-border-color: #e5e7eb; + -fx-border-width: 0 0 1 0; +} + +.file-status-title { + -fx-font-size: 16px; + -fx-font-weight: 600; + -fx-text-fill: #111827; +} + +.file-status-table { + -fx-background-color: white; +} + +.file-status-table .table-row-cell { + -fx-background-color: white; +} + +.file-status-table .table-row-cell:hover { + -fx-background-color: #f8fafc; +} + +.file-status-table .table-row-cell:selected { + -fx-background-color: #e0e7ff; +} + +/* 对话框样式 */ +.dialog-title { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-text-fill: #111827; +} + +/* 文件状态徽章 */ +.file-status-badge { + -fx-background-radius: 4; + -fx-padding: 2 8; + -fx-font-size: 12px; + -fx-font-weight: 500; +} + +.file-status-badge.modified { + -fx-background-color: #d1fae5; + -fx-text-fill: #065f46; +} + +.file-status-badge.added { + -fx-background-color: #dbeafe; + -fx-text-fill: #1e40af; +} + +.file-status-badge.deleted { + -fx-background-color: #fee2e2; + -fx-text-fill: #991b1b; +} + +.file-status-badge.conflicted { + -fx-background-color: #fef3c7; + -fx-text-fill: #92400e; +} diff --git a/src/main/resources/fxml/main.fxml b/src/main/resources/fxml/main.fxml new file mode 100644 index 0000000..fcff6d4 --- /dev/null +++ b/src/main/resources/fxml/main.fxml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/project-dialog.fxml b/src/main/resources/fxml/project-dialog.fxml new file mode 100644 index 0000000..2fa2ef8 --- /dev/null +++ b/src/main/resources/fxml/project-dialog.fxml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..5486341 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + ${user.home}/.svn-manager/svn-manager.log + + ${user.home}/.svn-manager/svn-manager.%d{yyyy-MM-dd}.log + 30 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/src/main/resources/projects.json b/src/main/resources/projects.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/main/resources/projects.json @@ -0,0 +1 @@ +[] diff --git a/ui-preview.html b/ui-preview.html new file mode 100644 index 0000000..137ecf9 --- /dev/null +++ b/ui-preview.html @@ -0,0 +1,418 @@ + + + + + + SVN管理器 - 多项目管理界面 + + + + + + + + + + + + + + + + + SVN管理器 + 多项目管理 + + + + + + + 添加新项目 + + + + + + + + + + + + + + + 项目A - 前端开发 + /Users/workspace/project-a + + + + 已同步 + + + + + + + + + + + + + + + + 项目B - 后端服务 + /Users/workspace/project-b + + + + 有更新 + + + + + + + + + + + + + + + + 项目C - 移动端 + /Users/workspace/project-c + + + + 未连接 + + + + + + + + + + + 共 3 个项目 + + + + + + + + + + + 项目A - 前端开发 + /Users/workspace/project-a + + + + + + + 刷新状态 + + + + + + 执行操作 + + + + + + + + + + + + + Checkout + + + + + + Update + + + + + + Commit + + + + + + Status + + + + + + Log + + + + + + Diff + + + + + + Info + + + + + + + + + + + 当前版本 + + + + + r1234 + 最新版本 + + + + + 工作副本 + + + + + r1230 + 需要更新 + + + + + 修改文件 + + + + + 5 + 待提交 + + + + + + + 文件状态 + + + + + + + + + + src/components/Header.vue + 已修改 + + + + M + + + + + + + + + + + + + + + + + src/utils/api.js + 冲突 + + + + C + + + + + + + + + + + + + + + + + src/styles/main.css + 已添加 + + + + A + + + + + + + + + + + + + + + + + \ No newline at end of file
多项目管理
/Users/workspace/project-a
/Users/workspace/project-b
/Users/workspace/project-c