chore: initial project setup
This commit is contained in:
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -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
|
||||
129
01-技术选型.md
Normal file
129
01-技术选型.md
Normal file
@@ -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提供现代化的用户体验
|
||||
94
README.md
Normal file
94
README.md
Normal file
@@ -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
|
||||
29
init-git.bat
Normal file
29
init-git.bat
Normal file
@@ -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
|
||||
52
init-git.ps1
Normal file
52
init-git.ps1
Normal file
@@ -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
|
||||
116
pom.xml
Normal file
116
pom.xml
Normal file
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.svnmanager</groupId>
|
||||
<artifactId>svn-manager</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>SVN Manager</name>
|
||||
<description>SVN管理工具 - 多项目管理界面</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<javafx.version>17.0.2</javafx.version>
|
||||
<jackson.version>2.15.2</jackson.version>
|
||||
<logback.version>1.4.8</logback.version>
|
||||
<slf4j.version>2.0.7</slf4j.version>
|
||||
<junit.version>5.10.0</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- JavaFX -->
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson for JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit 5 for testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Maven Compiler Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- JavaFX Maven Plugin -->
|
||||
<plugin>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>0.0.8</version>
|
||||
<configuration>
|
||||
<mainClass>com.svnmanager.MainApp</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Maven Surefire Plugin for tests -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
48
src/main/java/com/svnmanager/MainApp.java
Normal file
48
src/main/java/com/svnmanager/MainApp.java
Normal 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);
|
||||
}
|
||||
}
|
||||
559
src/main/java/com/svnmanager/controller/MainController.java
Normal file
559
src/main/java/com/svnmanager/controller/MainController.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/main/java/com/svnmanager/model/Project.java
Normal file
117
src/main/java/com/svnmanager/model/Project.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/main/java/com/svnmanager/model/SvnFileStatus.java
Normal file
91
src/main/java/com/svnmanager/model/SvnFileStatus.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/main/java/com/svnmanager/model/SvnInfo.java
Normal file
100
src/main/java/com/svnmanager/model/SvnInfo.java
Normal 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;
|
||||
}
|
||||
}
|
||||
72
src/main/java/com/svnmanager/model/SvnLog.java
Normal file
72
src/main/java/com/svnmanager/model/SvnLog.java
Normal 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);
|
||||
}
|
||||
}
|
||||
95
src/main/java/com/svnmanager/model/SvnStatus.java
Normal file
95
src/main/java/com/svnmanager/model/SvnStatus.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/main/java/com/svnmanager/service/CheckoutService.java
Normal file
67
src/main/java/com/svnmanager/service/CheckoutService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
134
src/main/java/com/svnmanager/service/CommitService.java
Normal file
134
src/main/java/com/svnmanager/service/CommitService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/main/java/com/svnmanager/service/DiffService.java
Normal file
64
src/main/java/com/svnmanager/service/DiffService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
84
src/main/java/com/svnmanager/service/InfoService.java
Normal file
84
src/main/java/com/svnmanager/service/InfoService.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/main/java/com/svnmanager/service/LogService.java
Normal file
139
src/main/java/com/svnmanager/service/LogService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/main/java/com/svnmanager/service/StatusService.java
Normal file
102
src/main/java/com/svnmanager/service/StatusService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
64
src/main/java/com/svnmanager/service/SvnService.java
Normal file
64
src/main/java/com/svnmanager/service/SvnService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/main/java/com/svnmanager/service/UpdateService.java
Normal file
126
src/main/java/com/svnmanager/service/UpdateService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
157
src/main/java/com/svnmanager/util/ConfigUtil.java
Normal file
157
src/main/java/com/svnmanager/util/ConfigUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
51
src/main/java/com/svnmanager/util/LogUtil.java
Normal file
51
src/main/java/com/svnmanager/util/LogUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
159
src/main/java/com/svnmanager/util/ProcessUtil.java
Normal file
159
src/main/java/com/svnmanager/util/ProcessUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/main/resources/application.properties
Normal file
11
src/main/resources/application.properties
Normal file
@@ -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
|
||||
377
src/main/resources/css/styles.css
Normal file
377
src/main/resources/css/styles.css
Normal file
@@ -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;
|
||||
}
|
||||
122
src/main/resources/fxml/main.fxml
Normal file
122
src/main/resources/fxml/main.fxml
Normal file
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
<?import java.net.URL?>
|
||||
|
||||
<BorderPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.svnmanager.controller.MainController">
|
||||
<stylesheets>
|
||||
<URL value="@../css/styles.css" />
|
||||
</stylesheets>
|
||||
|
||||
<!-- 左侧边栏 -->
|
||||
<left>
|
||||
<VBox fx:id="sidebar" styleClass="sidebar" prefWidth="320" spacing="0">
|
||||
<!-- 头部 -->
|
||||
<VBox styleClass="sidebar-header" spacing="12" style="-fx-padding: 24;">
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<Region prefWidth="40" prefHeight="40" styleClass="logo-icon"/>
|
||||
<VBox spacing="2">
|
||||
<Label text="SVN管理器" styleClass="app-title"/>
|
||||
<Label text="多项目管理" styleClass="app-subtitle"/>
|
||||
</VBox>
|
||||
</HBox>
|
||||
<Button fx:id="addProjectButton" text="添加新项目" styleClass="add-project-button" onAction="#handleAddProject" maxWidth="Infinity"/>
|
||||
</VBox>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS">
|
||||
<VBox fx:id="projectList" styleClass="project-list" spacing="8" style="-fx-padding: 16;">
|
||||
<!-- 项目卡片将通过代码动态添加 -->
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<VBox styleClass="sidebar-footer" style="-fx-padding: 16; -fx-border-color: #e2e8f0; -fx-border-width: 1 0 0 0;">
|
||||
<Label fx:id="projectCountLabel" text="共 0 个项目" styleClass="project-count"/>
|
||||
</VBox>
|
||||
</VBox>
|
||||
</left>
|
||||
|
||||
<!-- 右侧主内容区 -->
|
||||
<center>
|
||||
<VBox spacing="0" VBox.vgrow="ALWAYS">
|
||||
<!-- 顶部工具栏 -->
|
||||
<HBox styleClass="toolbar" alignment="CENTER_LEFT" spacing="16" style="-fx-padding: 16 24;">
|
||||
<VBox spacing="4" HBox.hgrow="ALWAYS">
|
||||
<Label fx:id="projectTitleLabel" text="请选择项目" styleClass="project-title-label"/>
|
||||
<Label fx:id="projectPathLabel" text="" styleClass="project-path-label"/>
|
||||
</VBox>
|
||||
<HBox spacing="12">
|
||||
<Button fx:id="refreshButton" text="刷新状态" styleClass="refresh-button" onAction="#handleRefresh"/>
|
||||
<Button fx:id="executeButton" text="执行操作" styleClass="execute-button" onAction="#handleExecute"/>
|
||||
</HBox>
|
||||
</HBox>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<HBox styleClass="action-buttons" spacing="12" style="-fx-padding: 16 24; -fx-background-color: white; -fx-border-color: #e2e8f0; -fx-border-width: 0 0 1 0;">
|
||||
<Button fx:id="checkoutButton" text="Checkout" styleClass="action-btn checkout-btn" onAction="#handleCheckout"/>
|
||||
<Button fx:id="updateButton" text="Update" styleClass="action-btn update-btn" onAction="#handleUpdate"/>
|
||||
<Button fx:id="commitButton" text="Commit" styleClass="action-btn commit-btn" onAction="#handleCommit"/>
|
||||
<Button fx:id="statusButton" text="Status" styleClass="action-btn status-btn" onAction="#handleStatus"/>
|
||||
<Button fx:id="logButton" text="Log" styleClass="action-btn log-btn" onAction="#handleLog"/>
|
||||
<Button fx:id="diffButton" text="Diff" styleClass="action-btn diff-btn" onAction="#handleDiff"/>
|
||||
<Button fx:id="infoButton" text="Info" styleClass="action-btn info-btn" onAction="#handleInfo"/>
|
||||
</HBox>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS" style="-fx-background-color: #f8fafc;">
|
||||
<VBox spacing="24" style="-fx-padding: 24;">
|
||||
<!-- 统计信息卡片 -->
|
||||
<HBox spacing="16" HBox.hgrow="ALWAYS">
|
||||
<!-- 当前版本卡片 -->
|
||||
<VBox styleClass="stat-card" HBox.hgrow="ALWAYS">
|
||||
<HBox alignment="CENTER_LEFT" spacing="8">
|
||||
<Label text="当前版本" styleClass="stat-label"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
<Label fx:id="currentVersionLabel" text="r0" styleClass="stat-value"/>
|
||||
<Label text="最新版本" styleClass="stat-description"/>
|
||||
</VBox>
|
||||
|
||||
<!-- 工作副本卡片 -->
|
||||
<VBox styleClass="stat-card" HBox.hgrow="ALWAYS">
|
||||
<HBox alignment="CENTER_LEFT" spacing="8">
|
||||
<Label text="工作副本" styleClass="stat-label"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
<Label fx:id="workingCopyLabel" text="r0" styleClass="stat-value"/>
|
||||
<Label text="需要更新" styleClass="stat-description"/>
|
||||
</VBox>
|
||||
|
||||
<!-- 修改文件卡片 -->
|
||||
<VBox styleClass="stat-card" HBox.hgrow="ALWAYS">
|
||||
<HBox alignment="CENTER_LEFT" spacing="8">
|
||||
<Label text="修改文件" styleClass="stat-label"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
</HBox>
|
||||
<Label fx:id="modifiedFilesLabel" text="0" styleClass="stat-value"/>
|
||||
<Label text="待提交" styleClass="stat-description"/>
|
||||
</VBox>
|
||||
</HBox>
|
||||
|
||||
<!-- 文件状态列表 -->
|
||||
<VBox styleClass="file-status-card" spacing="0">
|
||||
<HBox styleClass="file-status-header" alignment="CENTER_LEFT" style="-fx-padding: 16 24; -fx-background-color: #f8fafc;">
|
||||
<Label text="文件状态" styleClass="file-status-title"/>
|
||||
</HBox>
|
||||
<TableView fx:id="fileStatusTable" styleClass="file-status-table">
|
||||
<columns>
|
||||
<TableColumn fx:id="statusColumn" text="状态" prefWidth="60"/>
|
||||
<TableColumn fx:id="pathColumn" text="文件路径" prefWidth="400"/>
|
||||
<TableColumn fx:id="actionColumn" text="操作" prefWidth="80"/>
|
||||
</columns>
|
||||
</TableView>
|
||||
</VBox>
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
</VBox>
|
||||
</center>
|
||||
</BorderPane>
|
||||
47
src/main/resources/fxml/project-dialog.fxml
Normal file
47
src/main/resources/fxml/project-dialog.fxml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import java.net.URL?>
|
||||
|
||||
<DialogPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.svnmanager.controller.ProjectDialogController">
|
||||
<stylesheets>
|
||||
<URL value="@../css/styles.css" />
|
||||
</stylesheets>
|
||||
|
||||
<headerText>
|
||||
<Label text="添加/编辑项目" styleClass="dialog-title"/>
|
||||
</headerText>
|
||||
|
||||
<content>
|
||||
<VBox spacing="16" style="-fx-padding: 20;">
|
||||
<GridPane hgap="12" vgap="12">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints minWidth="100" prefWidth="100"/>
|
||||
<ColumnConstraints hgrow="ALWAYS" minWidth="300"/>
|
||||
</columnConstraints>
|
||||
|
||||
<!-- 项目名称 -->
|
||||
<Label text="项目名称:" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
|
||||
<TextField fx:id="nameField" promptText="请输入项目名称" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
|
||||
|
||||
<!-- 项目路径 -->
|
||||
<Label text="项目路径:" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
|
||||
<HBox spacing="8" GridPane.columnIndex="1" GridPane.rowIndex="1">
|
||||
<TextField fx:id="pathField" promptText="请输入项目路径" HBox.hgrow="ALWAYS"/>
|
||||
<Button text="浏览..." onAction="#handleBrowsePath"/>
|
||||
</HBox>
|
||||
|
||||
<!-- SVN URL -->
|
||||
<Label text="SVN URL:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
|
||||
<TextField fx:id="svnUrlField" promptText="请输入SVN仓库URL" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
||||
</GridPane>
|
||||
</VBox>
|
||||
</content>
|
||||
|
||||
<buttonTypes>
|
||||
<ButtonType text="确定" buttonData="OK_DONE"/>
|
||||
<ButtonType text="取消" buttonData="CANCEL_CLOSE"/>
|
||||
</buttonTypes>
|
||||
</DialogPane>
|
||||
26
src/main/resources/logback.xml
Normal file
26
src/main/resources/logback.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${user.home}/.svn-manager/svn-manager.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${user.home}/.svn-manager/svn-manager.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
|
||||
<logger name="com.svnmanager" level="DEBUG" />
|
||||
</configuration>
|
||||
1
src/main/resources/projects.json
Normal file
1
src/main/resources/projects.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
418
ui-preview.html
Normal file
418
ui-preview.html
Normal file
@@ -0,0 +1,418 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SVN管理器 - 多项目管理界面</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.project-card.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.project-card.active .project-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.file-item {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background: #f8fafc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
<!-- 左侧边栏 - 项目列表 -->
|
||||
<div class="sidebar w-80 bg-white flex flex-col">
|
||||
<!-- 头部 -->
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900">SVN管理器</h1>
|
||||
<p class="text-sm text-gray-500">多项目管理</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
添加新项目
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
<!-- 项目A -->
|
||||
<div class="project-card active p-4 rounded-xl cursor-pointer" onclick="selectProject('project-a')">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="project-icon w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-sm mb-1 truncate">项目A - 前端开发</h3>
|
||||
<p class="text-xs opacity-80 mb-2 truncate">/Users/workspace/project-a</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="status-badge text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700">
|
||||
<span class="status-dot bg-green-500"></span>
|
||||
已同步
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目B -->
|
||||
<div class="project-card p-4 rounded-xl cursor-pointer bg-gray-50 hover:bg-gray-100" onclick="selectProject('project-b')">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="project-icon w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-sm mb-1 truncate text-gray-900">项目B - 后端服务</h3>
|
||||
<p class="text-xs text-gray-500 mb-2 truncate">/Users/workspace/project-b</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="status-badge text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">
|
||||
<span class="status-dot bg-yellow-500"></span>
|
||||
有更新
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目C -->
|
||||
<div class="project-card p-4 rounded-xl cursor-pointer bg-gray-50 hover:bg-gray-100" onclick="selectProject('project-c')">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="project-icon w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-semibold text-sm mb-1 truncate text-gray-900">项目C - 移动端</h3>
|
||||
<p class="text-xs text-gray-500 mb-2 truncate">/Users/workspace/project-c</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="status-badge text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-700">
|
||||
<span class="status-dot bg-gray-400"></span>
|
||||
未连接
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="text-xs text-gray-500 text-center">
|
||||
共 3 个项目
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧主内容区 -->
|
||||
<div class="main-content flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900" id="project-title">项目A - 前端开发</h2>
|
||||
<p class="text-sm text-gray-500 mt-1" id="project-path">/Users/workspace/project-a</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="action-btn px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium rounded-lg text-sm">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
刷新状态
|
||||
</button>
|
||||
<button class="action-btn px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg text-sm">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
执行操作
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="bg-white px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<button class="action-btn px-4 py-2 bg-blue-50 hover:bg-blue-100 text-blue-700 font-medium rounded-lg text-sm border border-blue-200">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
|
||||
</svg>
|
||||
Checkout
|
||||
</button>
|
||||
<button class="action-btn px-4 py-2 bg-green-50 hover:bg-green-100 text-green-700 font-medium rounded-lg text-sm border border-green-200">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Update
|
||||
</button>
|
||||
<button class="action-btn px-4 py-2 bg-purple-50 hover:bg-purple-100 text-purple-700 font-medium rounded-lg text-sm border border-purple-200">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
|
||||
</svg>
|
||||
Commit
|
||||
</button>
|
||||
<button class="action-btn px-4 py-2 bg-yellow-50 hover:bg-yellow-100 text-yellow-700 font-medium rounded-lg text-sm border border-yellow-200">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
Status
|
||||
</button>
|
||||
<button class="action-btn px-4 py-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 font-medium rounded-lg text-sm border border-indigo-200">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Log
|
||||
</button>
|
||||
<button class="action-btn px-4 py-2 bg-gray-50 hover:bg-gray-100 text-gray-700 font-medium rounded-lg text-sm border border-gray-200">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
Diff
|
||||
</button>
|
||||
<button class="action-btn px-4 py-2 bg-gray-50 hover:bg-gray-100 text-gray-700 font-medium rounded-lg text-sm border border-gray-200">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Info
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<!-- 项目信息卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-white rounded-xl p-5 border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm text-gray-500">当前版本</span>
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">r1234</div>
|
||||
<div class="text-xs text-gray-500 mt-1">最新版本</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-5 border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm text-gray-500">工作副本</span>
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">r1230</div>
|
||||
<div class="text-xs text-gray-500 mt-1">需要更新</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-5 border border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-sm text-gray-500">修改文件</span>
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-gray-900">5</div>
|
||||
<div class="text-xs text-gray-500 mt-1">待提交</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件状态列表 -->
|
||||
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="font-semibold text-gray-900">文件状态</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div class="file-item px-6 py-4 flex items-center justify-between hover:bg-gray-50">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<div class="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-900">src/components/Header.vue</div>
|
||||
<div class="text-xs text-gray-500">已修改</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs px-2 py-1 bg-green-100 text-green-700 rounded">M</span>
|
||||
<button class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-item px-6 py-4 flex items-center justify-between hover:bg-gray-50">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<div class="w-2 h-2 rounded-full bg-yellow-500"></div>
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-900">src/utils/api.js</div>
|
||||
<div class="text-xs text-gray-500">冲突</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs px-2 py-1 bg-yellow-100 text-yellow-700 rounded">C</span>
|
||||
<button class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-item px-6 py-4 flex items-center justify-between hover:bg-gray-50">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-900">src/styles/main.css</div>
|
||||
<div class="text-xs text-gray-500">已添加</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded">A</span>
|
||||
<button class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 项目数据
|
||||
const projects = {
|
||||
'project-a': {
|
||||
title: '项目A - 前端开发',
|
||||
path: '/Users/workspace/project-a',
|
||||
version: 'r1234',
|
||||
workingCopy: 'r1230',
|
||||
modifiedFiles: 5
|
||||
},
|
||||
'project-b': {
|
||||
title: '项目B - 后端服务',
|
||||
path: '/Users/workspace/project-b',
|
||||
version: 'r2456',
|
||||
workingCopy: 'r2456',
|
||||
modifiedFiles: 2
|
||||
},
|
||||
'project-c': {
|
||||
title: '项目C - 移动端',
|
||||
path: '/Users/workspace/project-c',
|
||||
version: 'r1890',
|
||||
workingCopy: 'r1885',
|
||||
modifiedFiles: 0
|
||||
}
|
||||
};
|
||||
|
||||
function selectProject(projectId) {
|
||||
// 更新项目卡片状态
|
||||
document.querySelectorAll('.project-card').forEach(card => {
|
||||
card.classList.remove('active');
|
||||
card.classList.add('bg-gray-50', 'hover:bg-gray-100');
|
||||
});
|
||||
|
||||
const clickedCard = event.currentTarget;
|
||||
clickedCard.classList.add('active');
|
||||
clickedCard.classList.remove('bg-gray-50', 'hover:bg-gray-100');
|
||||
|
||||
// 更新主内容区
|
||||
const project = projects[projectId];
|
||||
if (project) {
|
||||
document.getElementById('project-title').textContent = project.title;
|
||||
document.getElementById('project-path').textContent = project.path;
|
||||
|
||||
// 更新统计信息
|
||||
const stats = document.querySelectorAll('.bg-white.rounded-xl.p-5');
|
||||
if (stats.length >= 3) {
|
||||
stats[0].querySelector('.text-2xl').textContent = project.version;
|
||||
stats[1].querySelector('.text-2xl').textContent = project.workingCopy;
|
||||
stats[2].querySelector('.text-2xl').textContent = project.modifiedFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user