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