重构项目结构,移除旧Java客户端,添加前后端目录
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
69
backend/pom.xml
Normal file
69
backend/pom.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.svnmanager</groupId>
|
||||
<artifactId>svn-manager-backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>SVN Manager Backend</name>
|
||||
<description>SVN 管理工具 - REST API 后端</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<jackson.version>2.15.2</jackson.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.tmatesoft.svnkit</groupId>
|
||||
<artifactId>svnkit</artifactId>
|
||||
<version>1.10.13</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.svnmanager;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SvnManagerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SvnManagerApplication.class, args);
|
||||
}
|
||||
}
|
||||
23
backend/src/main/java/com/svnmanager/config/CorsConfig.java
Normal file
23
backend/src/main/java/com/svnmanager/config/CorsConfig.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.svnmanager.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowCredentials(true);
|
||||
config.setAllowedOriginPatterns(List.of("*"));
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/api/**", config);
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.svnmanager.controller;
|
||||
|
||||
import com.svnmanager.model.Project;
|
||||
import com.svnmanager.util.ConfigUtil;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/projects")
|
||||
public class ProjectController {
|
||||
|
||||
@GetMapping
|
||||
public List<Project> list() {
|
||||
return ConfigUtil.loadProjects();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> add(@RequestBody Project project) {
|
||||
if (project.getName() == null || project.getName().trim().isEmpty()
|
||||
|| project.getPath() == null || project.getPath().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("项目名称和路径不能为空");
|
||||
}
|
||||
boolean saved = ConfigUtil.addProject(project);
|
||||
return saved ? ResponseEntity.ok(project) : ResponseEntity.internalServerError().build();
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
public ResponseEntity<?> update(@RequestBody Project project) {
|
||||
boolean ok = ConfigUtil.updateProject(project);
|
||||
return ok ? ResponseEntity.ok(project) : ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable String id) {
|
||||
boolean ok = ConfigUtil.deleteProject(id);
|
||||
return ok ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.svnmanager.controller;
|
||||
|
||||
import com.svnmanager.model.FileTreeItem;
|
||||
import com.svnmanager.model.Project;
|
||||
import com.svnmanager.model.SvnLog;
|
||||
import com.svnmanager.model.SvnStatus;
|
||||
import com.svnmanager.service.*;
|
||||
import com.svnmanager.util.ConfigUtil;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/svn")
|
||||
public class SvnController {
|
||||
|
||||
private final StatusService statusService;
|
||||
private final UpdateService updateService;
|
||||
private final CommitService commitService;
|
||||
private final LogService logService;
|
||||
private final DiffService diffService;
|
||||
private final InfoService infoService;
|
||||
private final FileTreeService fileTreeService;
|
||||
|
||||
public SvnController(StatusService statusService, UpdateService updateService,
|
||||
CommitService commitService, LogService logService,
|
||||
DiffService diffService, InfoService infoService,
|
||||
FileTreeService fileTreeService) {
|
||||
this.statusService = statusService;
|
||||
this.updateService = updateService;
|
||||
this.commitService = commitService;
|
||||
this.logService = logService;
|
||||
this.diffService = diffService;
|
||||
this.infoService = infoService;
|
||||
this.fileTreeService = fileTreeService;
|
||||
}
|
||||
|
||||
@GetMapping("/status/{projectId}")
|
||||
public ResponseEntity<?> status(@PathVariable String projectId) {
|
||||
Project project = ConfigUtil.getProjectById(projectId);
|
||||
if (project == null) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
SvnStatus status = statusService.getStatus(project.getPath(), project.getUsername(), project.getPassword());
|
||||
FileTreeItem fileTree = fileTreeService.buildFileTree(project.getPath(), status);
|
||||
return ResponseEntity.ok(new StatusResponse(status, fileTree));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/update/{projectId}")
|
||||
public ResponseEntity<?> update(@PathVariable String projectId) {
|
||||
Project project = ConfigUtil.getProjectById(projectId);
|
||||
if (project == null) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
UpdateService.UpdateResult r = updateService.update(project.getPath(), project.getUsername(), project.getPassword());
|
||||
return ResponseEntity.ok(r);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/commit/{projectId}")
|
||||
public ResponseEntity<?> commit(@PathVariable String projectId, @RequestBody CommitRequest body) {
|
||||
Project project = ConfigUtil.getProjectById(projectId);
|
||||
if (project == null) return ResponseEntity.notFound().build();
|
||||
if (body == null || body.message == null || body.message.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("提交消息不能为空");
|
||||
}
|
||||
try {
|
||||
CommitService.CommitResult r = commitService.commit(project.getPath(), body.message,
|
||||
project.getUsername(), project.getPassword());
|
||||
return ResponseEntity.ok(r);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/log/{projectId}")
|
||||
public ResponseEntity<?> log(@PathVariable String projectId,
|
||||
@RequestParam(defaultValue = "20") int limit) {
|
||||
Project project = ConfigUtil.getProjectById(projectId);
|
||||
if (project == null) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
List<SvnLog> logs = logService.getLog(project.getPath(), limit, project.getUsername(), project.getPassword());
|
||||
return ResponseEntity.ok(logs);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/diff/{projectId}")
|
||||
public ResponseEntity<?> diff(@PathVariable String projectId,
|
||||
@RequestParam(required = false) String path) {
|
||||
Project project = ConfigUtil.getProjectById(projectId);
|
||||
if (project == null) return ResponseEntity.notFound().build();
|
||||
try {
|
||||
String diff = diffService.getDiff(project.getPath(), path, project.getUsername(), project.getPassword());
|
||||
return ResponseEntity.ok(new DiffResponse(diff));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(value = "/file/{projectId}", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public ResponseEntity<?> fileContent(@PathVariable String projectId,
|
||||
@RequestParam String path) {
|
||||
Project project = ConfigUtil.getProjectById(projectId);
|
||||
if (project == null) return ResponseEntity.notFound().build();
|
||||
if (path == null || path.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("path 不能为空");
|
||||
}
|
||||
try {
|
||||
Path projectPath = Paths.get(project.getPath()).toAbsolutePath().normalize();
|
||||
Path filePath = projectPath.resolve(path.replace("\\", "/")).normalize();
|
||||
if (!filePath.startsWith(projectPath)) {
|
||||
return ResponseEntity.badRequest().body("无效路径");
|
||||
}
|
||||
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String content = Files.readString(filePath, StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE + "; charset=UTF-8")
|
||||
.body(content);
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.internalServerError().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static class StatusResponse {
|
||||
public SvnStatus status;
|
||||
public FileTreeItem fileTree;
|
||||
public StatusResponse(SvnStatus status, FileTreeItem fileTree) {
|
||||
this.status = status;
|
||||
this.fileTree = fileTree;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommitRequest {
|
||||
public String message;
|
||||
}
|
||||
|
||||
public static class DiffResponse {
|
||||
public String diff;
|
||||
public DiffResponse(String diff) { this.diff = diff; }
|
||||
}
|
||||
}
|
||||
81
backend/src/main/java/com/svnmanager/model/FileTreeItem.java
Normal file
81
backend/src/main/java/com/svnmanager/model/FileTreeItem.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.svnmanager.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件树节点模型
|
||||
* 用于TreeView展示文件和目录的层级结构
|
||||
*/
|
||||
public class FileTreeItem {
|
||||
private String name;
|
||||
private String path;
|
||||
private boolean isDirectory;
|
||||
private SvnFileStatus.FileStatus status;
|
||||
private List<FileTreeItem> children;
|
||||
|
||||
public FileTreeItem(String name, String path, boolean isDirectory) {
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
this.isDirectory = isDirectory;
|
||||
this.status = SvnFileStatus.FileStatus.NORMAL;
|
||||
this.children = new ArrayList<>();
|
||||
}
|
||||
|
||||
public FileTreeItem(String name, String path, boolean isDirectory, SvnFileStatus.FileStatus status) {
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
this.isDirectory = isDirectory;
|
||||
this.status = status;
|
||||
this.children = new ArrayList<>();
|
||||
}
|
||||
|
||||
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 boolean isDirectory() {
|
||||
return isDirectory;
|
||||
}
|
||||
|
||||
public void setDirectory(boolean directory) {
|
||||
isDirectory = directory;
|
||||
}
|
||||
|
||||
public SvnFileStatus.FileStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(SvnFileStatus.FileStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public List<FileTreeItem> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<FileTreeItem> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public void addChild(FileTreeItem child) {
|
||||
this.children.add(child);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
139
backend/src/main/java/com/svnmanager/model/Project.java
Normal file
139
backend/src/main/java/com/svnmanager/model/Project.java
Normal file
@@ -0,0 +1,139 @@
|
||||
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("username")
|
||||
private String username;
|
||||
|
||||
@JsonProperty("password")
|
||||
private String password;
|
||||
|
||||
@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 getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
backend/src/main/java/com/svnmanager/model/SvnInfo.java
Normal file
100
backend/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
backend/src/main/java/com/svnmanager/model/SvnLog.java
Normal file
72
backend/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
backend/src/main/java/com/svnmanager/model/SvnStatus.java
Normal file
95
backend/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.SVNURL;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.SVNDepth;
|
||||
import org.tmatesoft.svn.core.wc.SVNRevision;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Checkout 服务(基于 SVNKit)
|
||||
*/
|
||||
@Service
|
||||
public class CheckoutService extends SvnService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CheckoutService.class);
|
||||
|
||||
public boolean checkout(String svnUrl, String targetPath, String revision) {
|
||||
logger.info("检出仓库: {} 到 {}", svnUrl, targetPath);
|
||||
|
||||
SVNRevision rev = (revision != null && !revision.isEmpty())
|
||||
? SVNRevision.create(Long.parseLong(revision.replaceAll("\\D", "")))
|
||||
: SVNRevision.HEAD;
|
||||
|
||||
SVNClientManager clientManager = createClientManager(null, null);
|
||||
try {
|
||||
SVNURL url = SVNURL.parseURIEncoded(svnUrl);
|
||||
File target = new File(targetPath);
|
||||
clientManager.getUpdateClient().doCheckout(url, target, rev, rev, SVNDepth.INFINITY, true);
|
||||
logger.info("检出成功");
|
||||
return true;
|
||||
} catch (SVNException e) {
|
||||
logger.error("检出失败: {}", e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkout(String svnUrl, String targetPath) {
|
||||
return checkout(svnUrl, targetPath, null);
|
||||
}
|
||||
}
|
||||
115
backend/src/main/java/com/svnmanager/service/CommitService.java
Normal file
115
backend/src/main/java/com/svnmanager/service/CommitService.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tmatesoft.svn.core.SVNCommitInfo;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.SVNDepth;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Commit 服务(基于 SVNKit)
|
||||
*/
|
||||
@Service
|
||||
public class CommitService extends SvnService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CommitService.class);
|
||||
|
||||
public CommitResult commit(String workingDirectory, String message, List<String> files,
|
||||
String username, String password) {
|
||||
logger.info("提交修改: {}", workingDirectory);
|
||||
|
||||
if (!isValidWorkingCopy(workingDirectory)) {
|
||||
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
|
||||
}
|
||||
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("提交消息不能为空");
|
||||
}
|
||||
|
||||
File wcRoot = new File(workingDirectory);
|
||||
File[] commitPaths = (files != null && !files.isEmpty())
|
||||
? files.stream().map(f -> new File(wcRoot, f)).toArray(File[]::new)
|
||||
: new File[]{wcRoot};
|
||||
|
||||
CommitResult commitResult = new CommitResult();
|
||||
SVNClientManager clientManager = createClientManager(username, password);
|
||||
try {
|
||||
SVNCommitInfo info = clientManager.getCommitClient().doCommit(
|
||||
commitPaths,
|
||||
true,
|
||||
message,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
SVNDepth.INFINITY);
|
||||
commitResult.setSuccess(true);
|
||||
if (info.getNewRevision() >= 0) {
|
||||
commitResult.setRevision(String.valueOf(info.getNewRevision()));
|
||||
}
|
||||
logger.info("提交成功,版本: {}", commitResult.getRevision());
|
||||
} catch (SVNException e) {
|
||||
logger.error("提交失败: {}", e.getMessage());
|
||||
commitResult.setSuccess(false);
|
||||
commitResult.setError(e.getMessage());
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
|
||||
return commitResult;
|
||||
}
|
||||
|
||||
public CommitResult commit(String workingDirectory, String message, String username, String password) {
|
||||
return commit(workingDirectory, message, null, username, password);
|
||||
}
|
||||
|
||||
public CommitResult commit(String workingDirectory, String message) {
|
||||
return commit(workingDirectory, message, null, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.SVNDepth;
|
||||
import org.tmatesoft.svn.core.wc.SVNRevision;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Diff 服务(基于 SVNKit)
|
||||
*/
|
||||
@Service
|
||||
public class DiffService extends SvnService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(DiffService.class);
|
||||
|
||||
public String getDiff(String workingDirectory, String filePath, String username, String password) {
|
||||
logger.debug("获取差异: {}", workingDirectory);
|
||||
|
||||
if (!isValidWorkingCopy(workingDirectory)) {
|
||||
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
|
||||
}
|
||||
|
||||
File wcRoot = new File(workingDirectory);
|
||||
File[] paths = (filePath != null && !filePath.isEmpty())
|
||||
? new File[]{new File(wcRoot, filePath)}
|
||||
: new File[]{wcRoot};
|
||||
|
||||
// 使用双路径形式 (path@BASE vs path@WORKING),无需访问远程仓库,避免 E195002 pegged diff 错误
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
SVNClientManager clientManager = createClientManager(username, password);
|
||||
try {
|
||||
for (File path : paths) {
|
||||
clientManager.getDiffClient().doDiff(
|
||||
path,
|
||||
SVNRevision.BASE,
|
||||
path,
|
||||
SVNRevision.WORKING,
|
||||
SVNDepth.INFINITY,
|
||||
false,
|
||||
out,
|
||||
null);
|
||||
}
|
||||
return out.toString(StandardCharsets.UTF_8.name());
|
||||
} catch (SVNException e) {
|
||||
logger.warn("获取差异失败: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
} catch (java.io.UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public String getDiff(String workingDirectory) {
|
||||
return getDiff(workingDirectory, null, null, null);
|
||||
}
|
||||
|
||||
public String getDiff(String workingDirectory, String filePath) {
|
||||
return getDiff(workingDirectory, filePath, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import com.svnmanager.model.FileTreeItem;
|
||||
import com.svnmanager.model.SvnFileStatus;
|
||||
import com.svnmanager.model.SvnStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class FileTreeService {
|
||||
|
||||
public FileTreeItem buildFileTree(String projectPath, SvnStatus status) {
|
||||
File projectDir = new File(projectPath);
|
||||
FileTreeItem root = new FileTreeItem(projectDir.getName(), projectPath, true);
|
||||
root.setStatus(SvnFileStatus.FileStatus.NORMAL);
|
||||
|
||||
Map<String, FileTreeItem> pathMap = new HashMap<>();
|
||||
pathMap.put(projectPath, root);
|
||||
|
||||
for (SvnFileStatus fileStatus : status.getFiles()) {
|
||||
String relativePath = fileStatus.getPath();
|
||||
File file = new File(projectPath, relativePath);
|
||||
String fullPath = file.getAbsolutePath();
|
||||
String name = file.getName();
|
||||
|
||||
FileTreeItem parent = ensureParentExists(file.getParentFile(), pathMap, projectPath);
|
||||
FileTreeItem node = new FileTreeItem(name, fullPath, file.isDirectory(), fileStatus.getStatus());
|
||||
parent.getChildren().add(node);
|
||||
pathMap.put(fullPath, node);
|
||||
}
|
||||
|
||||
sortTree(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
private FileTreeItem ensureParentExists(File parent, Map<String, FileTreeItem> pathMap, String projectPath) {
|
||||
if (parent == null) return pathMap.get(projectPath);
|
||||
String parentPath = parent.getAbsolutePath();
|
||||
if (parentPath.equals(projectPath)) return pathMap.get(projectPath);
|
||||
if (pathMap.containsKey(parentPath)) return pathMap.get(parentPath);
|
||||
FileTreeItem grandParent = ensureParentExists(parent.getParentFile(), pathMap, projectPath);
|
||||
FileTreeItem dirItem = new FileTreeItem(parent.getName(), parentPath, true);
|
||||
grandParent.getChildren().add(dirItem);
|
||||
pathMap.put(parentPath, dirItem);
|
||||
return dirItem;
|
||||
}
|
||||
|
||||
private void sortTree(FileTreeItem item) {
|
||||
if (item.getChildren().isEmpty()) return;
|
||||
item.getChildren().sort(Comparator
|
||||
.comparing(FileTreeItem::isDirectory, Comparator.reverseOrder())
|
||||
.thenComparing(FileTreeItem::getName, String.CASE_INSENSITIVE_ORDER));
|
||||
item.getChildren().forEach(this::sortTree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import com.svnmanager.model.SvnInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.SVNNodeKind;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.wc.SVNRevision;
|
||||
import org.tmatesoft.svn.core.wc.SVNInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Info 服务(基于 SVNKit)
|
||||
*/
|
||||
@Service
|
||||
public class InfoService extends SvnService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(InfoService.class);
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public SvnInfo getInfo(String workingDirectory, String username, String password) {
|
||||
logger.debug("获取信息: {}", workingDirectory);
|
||||
|
||||
if (!isValidWorkingCopy(workingDirectory)) {
|
||||
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
|
||||
}
|
||||
|
||||
SVNClientManager clientManager = createClientManager(username, password);
|
||||
try {
|
||||
SVNInfo info = clientManager.getWCClient().doInfo(new File(workingDirectory), SVNRevision.WORKING);
|
||||
return mapToSvnInfo(info, workingDirectory);
|
||||
} catch (SVNException e) {
|
||||
logger.warn("获取信息失败: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public SvnInfo getInfo(String workingDirectory) {
|
||||
return getInfo(workingDirectory, null, null);
|
||||
}
|
||||
|
||||
private static SvnInfo mapToSvnInfo(SVNInfo info, String workingDirectory) {
|
||||
SvnInfo out = new SvnInfo();
|
||||
if (info.getFile() != null) {
|
||||
out.setPath(info.getFile().getAbsolutePath());
|
||||
} else {
|
||||
out.setPath(workingDirectory);
|
||||
}
|
||||
if (info.getURL() != null) {
|
||||
out.setUrl(info.getURL().toString());
|
||||
}
|
||||
if (info.getRepositoryRootURL() != null) {
|
||||
out.setRepositoryRoot(info.getRepositoryRootURL().toString());
|
||||
}
|
||||
out.setRepositoryUuid(info.getRepositoryUUID());
|
||||
if (info.getRevision() != null && info.getRevision().isValid()) {
|
||||
out.setRevision(String.valueOf(info.getRevision().getNumber()));
|
||||
}
|
||||
SVNNodeKind kind = info.getKind();
|
||||
if (kind != null) {
|
||||
out.setNodeKind(kind.toString());
|
||||
}
|
||||
String schedule = info.getSchedule();
|
||||
out.setSchedule(schedule != null ? schedule : "normal");
|
||||
out.setLastChangedAuthor(info.getAuthor());
|
||||
if (info.getCommittedRevision() != null && info.getCommittedRevision().isValid()) {
|
||||
out.setLastChangedRev(String.valueOf(info.getCommittedRevision().getNumber()));
|
||||
}
|
||||
if (info.getCommittedDate() != null) {
|
||||
out.setLastChangedDate(LocalDateTime.ofInstant(info.getCommittedDate().toInstant(), ZoneId.systemDefault()).format(DATE_FORMAT));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
81
backend/src/main/java/com/svnmanager/service/LogService.java
Normal file
81
backend/src/main/java/com/svnmanager/service/LogService.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import com.svnmanager.model.SvnLog;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.SVNLogEntryPath;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.wc.SVNRevision;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Log 服务(基于 SVNKit)
|
||||
*/
|
||||
@Service
|
||||
public class LogService extends SvnService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LogService.class);
|
||||
|
||||
public List<SvnLog> getLog(String workingDirectory, Integer limit, String username, String password) {
|
||||
logger.debug("获取日志: {}", workingDirectory);
|
||||
|
||||
if (!isValidWorkingCopy(workingDirectory)) {
|
||||
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
|
||||
}
|
||||
|
||||
List<SvnLog> logs = new ArrayList<>();
|
||||
File wcRoot = new File(workingDirectory);
|
||||
int limitVal = (limit != null && limit > 0) ? limit : 50;
|
||||
|
||||
SVNClientManager clientManager = createClientManager(username, password);
|
||||
try {
|
||||
clientManager.getLogClient().doLog(
|
||||
new File[]{wcRoot},
|
||||
SVNRevision.HEAD,
|
||||
SVNRevision.create(0),
|
||||
SVNRevision.HEAD,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
limitVal,
|
||||
null,
|
||||
entry -> {
|
||||
SvnLog log = new SvnLog();
|
||||
log.setRevision(String.valueOf(entry.getRevision()));
|
||||
log.setAuthor(entry.getAuthor());
|
||||
if (entry.getDate() != null) {
|
||||
log.setDate(LocalDateTime.ofInstant(entry.getDate().toInstant(), ZoneId.systemDefault()));
|
||||
}
|
||||
log.setMessage(entry.getMessage() != null ? entry.getMessage() : "");
|
||||
if (entry.getChangedPaths() != null) {
|
||||
for (Map.Entry<String, SVNLogEntryPath> e : entry.getChangedPaths().entrySet()) {
|
||||
log.addChangedPath(e.getKey());
|
||||
}
|
||||
}
|
||||
logs.add(log);
|
||||
});
|
||||
} catch (SVNException e) {
|
||||
logger.warn("获取日志失败: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
public List<SvnLog> getLog(String workingDirectory, String username, String password) {
|
||||
return getLog(workingDirectory, 50, username, password);
|
||||
}
|
||||
|
||||
public List<SvnLog> getLog(String workingDirectory) {
|
||||
return getLog(workingDirectory, 50, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import com.svnmanager.model.SvnFileStatus;
|
||||
import com.svnmanager.model.SvnStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.wc.SVNStatus;
|
||||
import org.tmatesoft.svn.core.wc.SVNStatusType;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Status 服务(基于 SVNKit)
|
||||
*/
|
||||
@Service
|
||||
public class StatusService extends SvnService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StatusService.class);
|
||||
|
||||
public SvnStatus getStatus(String workingDirectory, String username, String password) {
|
||||
logger.debug("获取状态: {}", workingDirectory);
|
||||
|
||||
if (!isValidWorkingCopy(workingDirectory)) {
|
||||
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
|
||||
}
|
||||
|
||||
File wcRoot = new File(workingDirectory);
|
||||
String wcRootPath = wcRoot.getAbsolutePath();
|
||||
List<SvnFileStatus> files = new ArrayList<>();
|
||||
|
||||
SVNClientManager clientManager = createClientManager(username, password);
|
||||
try {
|
||||
// reportAll=true: 列出所有版本化/未版本化项,与 svn status -v 一致,供右侧文件树完整展示
|
||||
clientManager.getStatusClient().doStatus(wcRoot, true, false, true, false, new org.tmatesoft.svn.core.wc.ISVNStatusHandler() {
|
||||
@Override
|
||||
public void handleStatus(SVNStatus status) {
|
||||
SvnFileStatus.FileStatus fs = mapStatusType(status.getContentsStatus());
|
||||
File file = status.getFile();
|
||||
if (file == null) return;
|
||||
String absPath = file.getAbsolutePath();
|
||||
String relativePath = absPath.startsWith(wcRootPath)
|
||||
? absPath.substring(wcRootPath.length()).replace(File.separatorChar, '/').replaceAll("^/", "")
|
||||
: file.getName();
|
||||
if (relativePath.isEmpty()) return;
|
||||
SvnFileStatus f = new SvnFileStatus(relativePath, fs);
|
||||
f.setWorkingCopyStatus(String.valueOf(fs.getCode()));
|
||||
files.add(f);
|
||||
}
|
||||
});
|
||||
} catch (SVNException e) {
|
||||
logger.warn("获取状态失败: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
|
||||
SvnStatus status = new SvnStatus();
|
||||
status.setWorkingCopyPath(workingDirectory);
|
||||
status.setFiles(files);
|
||||
return status;
|
||||
}
|
||||
|
||||
public SvnStatus getStatus(String workingDirectory) {
|
||||
return getStatus(workingDirectory, null, null);
|
||||
}
|
||||
|
||||
private static SvnFileStatus.FileStatus mapStatusType(SVNStatusType t) {
|
||||
if (t == null) return SvnFileStatus.FileStatus.NORMAL;
|
||||
if (t == SVNStatusType.STATUS_MODIFIED) return SvnFileStatus.FileStatus.MODIFIED;
|
||||
if (t == SVNStatusType.STATUS_ADDED) return SvnFileStatus.FileStatus.ADDED;
|
||||
if (t == SVNStatusType.STATUS_DELETED) return SvnFileStatus.FileStatus.DELETED;
|
||||
if (t == SVNStatusType.STATUS_CONFLICTED) return SvnFileStatus.FileStatus.CONFLICTED;
|
||||
if (t == SVNStatusType.STATUS_UNVERSIONED) return SvnFileStatus.FileStatus.UNVERSIONED;
|
||||
if (t == SVNStatusType.STATUS_MISSING) return SvnFileStatus.FileStatus.MISSING;
|
||||
if (t == SVNStatusType.STATUS_IGNORED) return SvnFileStatus.FileStatus.IGNORED;
|
||||
if (t == SVNStatusType.STATUS_EXTERNAL) return SvnFileStatus.FileStatus.EXTERNAL;
|
||||
return SvnFileStatus.FileStatus.NORMAL;
|
||||
}
|
||||
}
|
||||
53
backend/src/main/java/com/svnmanager/service/SvnService.java
Normal file
53
backend/src/main/java/com/svnmanager/service/SvnService.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.wc.SVNWCUtil;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* SVN服务基类(基于 SVNKit)
|
||||
*/
|
||||
public abstract class SvnService {
|
||||
protected static final Logger logger = LoggerFactory.getLogger(SvnService.class);
|
||||
|
||||
/**
|
||||
* 创建带可选认证的 SVNClientManager。
|
||||
*
|
||||
* @param username 用户名(可为空)
|
||||
* @param password 密码(可为空)
|
||||
* @return SVNClientManager 实例,调用方使用后需 dispose
|
||||
*/
|
||||
protected SVNClientManager createClientManager(String username, String password) {
|
||||
if (username != null && !username.isEmpty()) {
|
||||
return SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true), username, password != null ? password : "");
|
||||
}
|
||||
return SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工作目录是否为有效的 SVN 工作副本(基于 SVNKit)。
|
||||
*/
|
||||
protected boolean isValidWorkingCopy(String workingDirectory) {
|
||||
if (workingDirectory == null || workingDirectory.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
File dir = new File(workingDirectory);
|
||||
if (!dir.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
SVNClientManager clientManager = createClientManager(null, null);
|
||||
try {
|
||||
clientManager.getStatusClient().doStatus(dir, false);
|
||||
return true;
|
||||
} catch (SVNException e) {
|
||||
logger.debug("验证工作副本失败: {}", e.getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.svnmanager.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
import org.tmatesoft.svn.core.wc.SVNClientManager;
|
||||
import org.tmatesoft.svn.core.SVNDepth;
|
||||
import org.tmatesoft.svn.core.wc.SVNRevision;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Update 服务(基于 SVNKit)
|
||||
*/
|
||||
@Service
|
||||
public class UpdateService extends SvnService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(UpdateService.class);
|
||||
|
||||
public UpdateResult update(String workingDirectory, String revision, String username, String password) {
|
||||
logger.info("更新工作副本: {}", workingDirectory);
|
||||
|
||||
if (!isValidWorkingCopy(workingDirectory)) {
|
||||
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
|
||||
}
|
||||
|
||||
File wcRoot = new File(workingDirectory);
|
||||
SVNRevision targetRevision = (revision != null && !revision.isEmpty())
|
||||
? SVNRevision.create(Long.parseLong(revision.replaceAll("\\D", "")))
|
||||
: SVNRevision.HEAD;
|
||||
|
||||
UpdateResult updateResult = new UpdateResult();
|
||||
SVNClientManager clientManager = createClientManager(username, password);
|
||||
try {
|
||||
long rev = clientManager.getUpdateClient().doUpdate(wcRoot, targetRevision, SVNDepth.INFINITY, true, true);
|
||||
updateResult.setSuccess(true);
|
||||
updateResult.setRevision(String.valueOf(rev));
|
||||
logger.info("更新成功,版本: {}", rev);
|
||||
} catch (SVNException e) {
|
||||
logger.error("更新失败: {}", e.getMessage());
|
||||
updateResult.setSuccess(false);
|
||||
updateResult.setError(e.getMessage());
|
||||
} finally {
|
||||
clientManager.dispose();
|
||||
}
|
||||
|
||||
return updateResult;
|
||||
}
|
||||
|
||||
public UpdateResult update(String workingDirectory, String username, String password) {
|
||||
return update(workingDirectory, null, username, password);
|
||||
}
|
||||
|
||||
public UpdateResult update(String workingDirectory) {
|
||||
return update(workingDirectory, null, null, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
162
backend/src/main/java/com/svnmanager/util/ConfigUtil.java
Normal file
162
backend/src/main/java/com/svnmanager/util/ConfigUtil.java
Normal file
@@ -0,0 +1,162 @@
|
||||
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())) {
|
||||
Project existing = projects.get(i);
|
||||
// 密码留空时保留原密码
|
||||
if (project.getPassword() == null || project.getPassword().isEmpty()) {
|
||||
project.setPassword(existing.getPassword());
|
||||
}
|
||||
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
backend/src/main/java/com/svnmanager/util/LogUtil.java
Normal file
51
backend/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);
|
||||
}
|
||||
}
|
||||
2
backend/src/main/resources/application.properties
Normal file
2
backend/src/main/resources/application.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
server.port=8080
|
||||
spring.application.name=svn-manager-backend
|
||||
23
backend/src/main/resources/logback.xml
Normal file
23
backend/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user