使用jenkins自动化合并svn代码
·
最近工作中在使用svn进行代码合并时,手动操作很费时费力废我,且办公电脑卡的要命,合并提交一次代码需要耗费我好几分钟,想着研究个方法使用jenkins流水线进行合并。
自己编写java类,打成jar包,通过jenkins前端传入svn号,调用jar包进行合并代码。jenkins流水线中配置接收的参数类型为Multi-line String Parameter,可以填入多个svn号,各个svn号换行分割,java执行时进行遍历svn号依次进行代码合并。(代码中逻辑自行修改,本代码中修改的逻辑仅适用我自己)
注:最开始写了方式一和方式二,但各有优缺点,所以最终优化出了第三版本方式三;
强烈推荐使用方式三 。
1.创建自由风格流水线,配置如下


2.编写java代码
方式一(不推荐):
优点:
- svn所有操作均使用官方提供的svnkit库,官方保障
缺点:
- 合代码分支固定,无法根据路径进行更改;
- 仓库层级较浅,如果代码库代码量超级庞大,则svn操作会很慢;
- 在更新代码前使用doRevert方法还原,无法回退非版本管控下的文件
pom.xml
<?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>org.example</groupId>
<artifactId>mergeCode</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
<dependency>
<groupId>org.tmatesoft.svnkit</groupId>
<artifactId>svnkit</artifactId>
<version>1.10.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>org.example.Main</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
SVNMergeTool.java
package org.example.utils;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.*;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
/**
* svn合并代码
* 优点:svn所有操作均使用官方提供的svnkit库,官方保障
* 缺点:合代码分支固定,无法根据路径进行更改;
* 仓库层级较浅,如果代码库代码量超级庞大,则svn操作会很慢
*/
@Slf4j
public class SVNMergeTool {
// SVN仓库地址
private static final String SIT_REPOSITORY = "http://****/svn/****/branches/sit";
private static final String UAT_REPOSITORY = "http://****/svn/****/test/uat";
// 本地工作副本目录
private static final String LOCAL_WORKING_COPY = "/temp/UAT";
// SVN认证信息
private static final String USERNAME = "***";
private static final String PASSWORD = "***";
public static void merge(String sitVision) {
// 初始化SVNKit库
setupLibrary();
long sitRevision = Long.parseLong(sitVision);
log.info("获取的SIT分支的版本号: {}", sitRevision);
try {
// 执行合并操作
mergeSitToUat(sitRevision);
log.info("合并成功完成!");
log.info("=======================================================================================================");
} catch (SVNException e) {
log.error("合并过程中发生错误: " + e.getMessage());
e.printStackTrace();
log.info("=======================================================================================================");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void setupLibrary() {
// 初始化SVNKit库
SVNRepositoryFactoryImpl.setup();
// 不再需要调用 SVNClientManager.setup();
}
private static void mergeSitToUat(long sitRevision) throws SVNException, IOException {
// 创建认证管理器
ISVNAuthenticationManager authManager =
SVNWCUtil.createDefaultAuthenticationManager(USERNAME, PASSWORD.toCharArray());
// 创建SVN客户端管理器 - 使用新的API
SVNClientManager clientManager = SVNClientManager.newInstance(
new DefaultSVNOptions(), authManager);
// 获取UAT工作副本
SVNUpdateClient updateClient = clientManager.getUpdateClient();
updateClient.setIgnoreExternals(false);
File workingCopyDir = new File(LOCAL_WORKING_COPY);
// 检查工作副本是否存在,如果不存在则checkout
if (!workingCopyDir.exists() || !SVNWCUtil.isWorkingCopyRoot(workingCopyDir)) {
log.info("正在检出UAT工作副本...");
updateClient.doCheckout(
SVNURL.parseURIEncoded(UAT_REPOSITORY),
workingCopyDir,
SVNRevision.HEAD,
SVNRevision.HEAD,
SVNDepth.INFINITY,
false
);
} else {
// 在更新前先执行还原操作,确保工作副本干净
log.info("正在还原UAT工作副本...");
revertWorkingCopy(clientManager, workingCopyDir);
log.info("正在更新UAT工作副本...");
updateClient.doUpdate(
workingCopyDir,
SVNRevision.HEAD,
SVNDepth.INFINITY,
false,
false
);
}
// 获取SIT版本的日志信息
SVNRepository sitRepository = SVNRepositoryFactory.create(
SVNURL.parseURIEncoded(SIT_REPOSITORY));
sitRepository.setAuthenticationManager(authManager);
// 获取SIT版本的提交信息和作者
Collection<SVNLogEntry> logEntries = sitRepository.log(
new String[]{""}, // 空数组表示根目录
null,
sitRevision,
sitRevision,
true,
true
);
if (logEntries.isEmpty()) {
SVNErrorMessage e = SVNErrorMessage.create(SVNErrorCode.UNKNOWN,
"无法找到版本 " + sitRevision + " 的日志信息");
throw new SVNException(e);
}
SVNLogEntry logEntry = logEntries.iterator().next();
//sit分支提交的log
String sitLogMessage = logEntry.getMessage().trim();
//sit分支提交的作者
String sitAuthor = logEntry.getAuthor();
//sit分支提交代码列表清单
Map<String, SVNLogEntryPath> changedPaths = logEntry.getChangedPaths();
//代码服务名
String serverName = "【" + (CollectionUtil.newArrayList(changedPaths.keySet())).get(0).trim().split("/")[4] + "】";
String serverName1 = serverName.replace("【", "[").replace("】", "]");
//遍历代码清单
log.info("要合并的代码清单:");
for (String path : changedPaths.keySet()) {
log.info(path);
/* SVNLogEntryPath svnLogEntryPath = changedPaths.get(path);
//更改类型: 'A' 添加, 'D' 删除, 'M' 修改, 'R' 替换等
System.out.println(svnLogEntryPath.getType());*/
}
// 构建提交消息 例如:【icap-mbank】 新增登录controller xuezhiqian 52158
String commitMessage;
if (sitLogMessage.startsWith(serverName)) {
commitMessage = String.format("%s %s %d",
sitLogMessage, sitAuthor, sitRevision);
} else if (sitLogMessage.startsWith(serverName1)) {
commitMessage = String.format("%s %s %d",
sitLogMessage.replace(serverName1, serverName), sitAuthor, sitRevision);
} else {
commitMessage = String.format("%s %s %s %d",
serverName, sitLogMessage, sitAuthor, sitRevision);
}
log.info("uat分支提交log信息:");
log.info(commitMessage);
log.info("正在合并SIT版本 " + sitRevision + " 到 UAT...");
// 执行合并操作
SVNDiffClient diffClient = clientManager.getDiffClient();
diffClient.doMerge(
SVNURL.parseURIEncoded(SIT_REPOSITORY),
SVNRevision.create(sitRevision - 1),
SVNURL.parseURIEncoded(SIT_REPOSITORY),
SVNRevision.create(sitRevision),
workingCopyDir,
SVNDepth.INFINITY,
true,
false,
false,
false
);
// 检查是否有冲突
SVNStatusClient statusClient = clientManager.getStatusClient();
SVNStatus status = statusClient.doStatus(workingCopyDir, false);
if (status != null && status.getContentsStatus() == SVNStatusType.STATUS_CONFLICTED) {
log.error("警告: 合并过程中发现冲突,请手动解决冲突后重新提交!");
return;
}
// 提交更改
log.info("正在提交更改到UAT...");
SVNCommitClient commitClient = clientManager.getCommitClient();
SVNCommitInfo commitInfo = commitClient.doCommit(
new File[]{workingCopyDir},
false,
commitMessage,
null,
null,
false,
false,
SVNDepth.INFINITY
);
log.info("提交完成! 新版本号: " + commitInfo.getNewRevision());
}
/**
* 还原工作副本中的所有本地修改
*/
private static void revertWorkingCopy(SVNClientManager clientManager, File workingCopyDir)
throws SVNException {
SVNWCClient wcClient = clientManager.getWCClient();
// 还原整个工作副本,包括所有子目录
wcClient.doRevert(
new File[]{workingCopyDir},
SVNDepth.INFINITY,
null
);
}
/**
* 获取指定版本的所有变更路径
*/
private static SVNLogEntryPath[] getChangedPathsForRevision(SVNRepository repository, long revision)
throws SVNException {
Collection<SVNLogEntry> logEntries = repository.log(
new String[]{""},
null,
revision,
revision,
true,
true
);
if (logEntries.isEmpty()) {
return new SVNLogEntryPath[0];
}
SVNLogEntry logEntry = logEntries.iterator().next();
return logEntry.getChangedPaths().values().toArray(new SVNLogEntryPath[0]);
}
/**
* 合并单个文件
*/
private static void mergeSingleFile(
SVNClientManager clientManager,
File workingCopyDir,
String sitPath,
String uatPath,
long sitRevision) throws SVNException {
SVNDiffClient diffClient = clientManager.getDiffClient();
try {
// 获取SIT文件的URL
SVNURL sitFileUrl = SVNURL.parseURIEncoded(SIT_REPOSITORY + sitPath);
// 获取UAT工作副本中的文件路径
File uatFile = new File(workingCopyDir, uatPath);
// 确保目标目录存在
uatFile.getParentFile().mkdirs();
// 执行合并
diffClient.doMerge(
sitFileUrl,
SVNRevision.create(sitRevision - 1),
sitFileUrl,
SVNRevision.create(sitRevision),
uatFile,
SVNDepth.EMPTY,
true,
false,
false,
false
);
} catch (SVNException e) {
log.error("合并文件失败: " + sitPath + " -> " + uatPath + ": " + e.getMessage());
// 可以选择继续处理其他文件
}
}
}
Main.java
package org.example;
import lombok.extern.slf4j.Slf4j;
import org.example.utils.SVNMergeTool;
import java.io.IOException;
@Slf4j
public class Main {
public static void main(String[] args) throws IOException {
String arg = args[0];
for (String sitRevision : arg.trim().split("\n")) {
SVNMergeTool.merge(sitRevision.trim());
}
}
}
方式二(推荐):
优点:
- 代码分支可以代码中自定义更改路径匹配规则,通用性强;
- 仓库层级较相对方式一更深,更加精确的路径下进行svn操作,会相对较快;
- 使用命令还原代码,且可以删除非版本管控下的文件,还原的干净
缺点:
- svn部分操作均通过java执行命令方式进行,需自行更改逻辑或代码
pom.xml 同上
LinuxCommandExecutor.java
package org.example.utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Linux命令执行工具类
* 提供多种重载方法执行Linux命令
*/
public class LinuxCommandExecutor {
/**
* 执行Linux命令并返回结果
*
* @param command 要执行的命令
* @return 命令执行结果
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static String executeCommand(String command) throws IOException, InterruptedException {
return executeCommand(command, null, null, 0);
}
/**
* 执行Linux命令并返回结果
*
* @param command 要执行的命令
* @param timeout 超时时间(秒)
* @return 命令执行结果
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static String executeCommand(String command, long timeout) throws IOException, InterruptedException {
return executeCommand(command, null, null, timeout);
}
/**
* 执行Linux命令并返回结果
*
* @param command 要执行的命令
* @param workingDirectory 工作目录
* @return 命令执行结果
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static String executeCommand(String command, File workingDirectory) throws IOException, InterruptedException {
return executeCommand(command, workingDirectory, null, 0);
}
/**
* 执行Linux命令并返回结果
*
* @param command 要执行的命令
* @param workingDirectory 工作目录
* @param timeout 超时时间(秒)
* @return 命令执行结果
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static String executeCommand(String command, File workingDirectory, long timeout) throws IOException, InterruptedException {
return executeCommand(command, workingDirectory, null, timeout);
}
/**
* 执行Linux命令并返回结果
*
* @param command 要执行的命令
* @param envVars 环境变量
* @return 命令执行结果
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static String executeCommand(String command, String[] envVars) throws IOException, InterruptedException {
return executeCommand(command, null, envVars, 0);
}
/**
* 执行Linux命令并返回结果
*
* @param command 要执行的命令
* @param envVars 环境变量
* @param timeout 超时时间(秒)
* @return 命令执行结果
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static String executeCommand(String command, String[] envVars, long timeout) throws IOException, InterruptedException {
return executeCommand(command, null, envVars, timeout);
}
/**
* 执行Linux命令并返回结果
*
* @param command 要执行的命令
* @param workingDirectory 工作目录
* @param envVars 环境变量
* @param timeout 超时时间(秒)
* @return 命令执行结果
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static String executeCommand(String command, File workingDirectory, String[] envVars, long timeout)
throws IOException, InterruptedException {
// 使用ProcessBuilder执行命令
ProcessBuilder processBuilder = new ProcessBuilder("/bin/bash", "-c", command);
// 设置工作目录
if (workingDirectory != null) {
processBuilder.directory(workingDirectory);
}
// 设置环境变量
if (envVars != null) {
for (String envVar : envVars) {
String[] parts = envVar.split("=", 2);
if (parts.length == 2) {
processBuilder.environment().put(parts[0], parts[1]);
}
}
}
// 重定向错误流到标准输出
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
// 读取命令输出
StringBuilder output = new StringBuilder();
try (InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
// 等待命令执行完成或超时
if (timeout > 0) {
if (!process.waitFor(timeout, TimeUnit.SECONDS)) {
process.destroy();
throw new InterruptedException("Command execution timed out after " + timeout + " seconds");
}
} else {
process.waitFor();
}
return output.toString();
}
/**
* 执行Linux命令并返回结果和退出码
*
* @param command 要执行的命令
* @return 包含退出码和输出的CommandResult对象
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static CommandResult executeCommandWithExitCode(String command) throws IOException, InterruptedException {
return executeCommandWithExitCode(command, null, null, 0);
}
/**
* 执行Linux命令并返回结果和退出码
*
* @param command 要执行的命令
* @param timeout 超时时间(秒)
* @return 包含退出码和输出的CommandResult对象
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static CommandResult executeCommandWithExitCode(String command, long timeout) throws IOException, InterruptedException {
return executeCommandWithExitCode(command, null, null, timeout);
}
/**
* 执行Linux命令并返回结果和退出码
*
* @param command 要执行的命令
* @param workingDirectory 工作目录
* @param envVars 环境变量
* @param timeout 超时时间(秒)
* @return 包含退出码和输出的CommandResult对象
* @throws IOException 如果发生I/O错误
* @throws InterruptedException 如果执行过程被中断
*/
public static CommandResult executeCommandWithExitCode(String command, File workingDirectory, String[] envVars, long timeout)
throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder("/bin/bash", "-c", command);
if (workingDirectory != null) {
processBuilder.directory(workingDirectory);
}
if (envVars != null) {
for (String envVar : envVars) {
String[] parts = envVar.split("=", 2);
if (parts.length == 2) {
processBuilder.environment().put(parts[0], parts[1]);
}
}
}
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
StringBuilder output = new StringBuilder();
try (InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
int exitCode;
if (timeout > 0) {
if (process.waitFor(timeout, TimeUnit.SECONDS)) {
exitCode = process.exitValue();
} else {
process.destroy();
throw new InterruptedException("Command execution timed out after " + timeout + " seconds");
}
} else {
exitCode = process.waitFor();
}
return new CommandResult(exitCode, output.toString());
}
/**
* 命令执行结果封装类
*/
public static class CommandResult {
private final int exitCode;
private final String output;
public CommandResult(int exitCode, String output) {
this.exitCode = exitCode;
this.output = output;
}
public int getExitCode() {
return exitCode;
}
public String getOutput() {
return output;
}
@Override
public String toString() {
return "Exit Code: " + exitCode + "\nOutput:\n" + output;
}
}
/**
* 测试方法
*/
public static void main(String[] args) {
try {
// 示例1:执行简单命令
System.out.println("=== 执行 'ls -l' 命令 ===");
String result1 = executeCommand("ls -l");
System.out.println(result1);
// 示例2:执行带超时的命令
System.out.println("=== 执行 'sleep 2' 命令(带超时)===");
String result2 = executeCommand("sleep 2", 3);
System.out.println("命令执行完成: " + result2);
// 示例3:执行命令并获取退出码
System.out.println("=== 执行 'pwd' 命令并获取退出码 ===");
CommandResult result3 = executeCommandWithExitCode("pwd");
System.out.println(result3);
// 示例4:设置环境变量执行命令
System.out.println("=== 设置环境变量执行命令 ===");
String[] envVars = {"MY_VAR=HelloWorld"};
String result4 = executeCommand("echo $MY_VAR", envVars);
System.out.println(result4);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
StringUtils.java
package org.example.utils;
public class StringUtils {
/**
* 通用方法:获取字符串中第n个字符的索引(n从1开始)
*
* @param str 目标字符串(如文件路径)
* @param n 要查找的字符的序号(如第五个则n=5)
* @return 第n个字符的索引,若不存在则返回-1
*/
public static int getNthSlashIndex(String str, String chars, int n) {
// 非法输入校验(字符串为null或n≤0)
if (str == null || n <= 0) {
return -1;
}
int currentIndex = -1; // 初始查找位置(从字符串开头前一位开始)
for (int i = 0; i < n; i++) {
// 从“上一个/的下一位”开始查找下一个/
currentIndex = str.indexOf(chars, currentIndex + 1);
// 若中途找不到“/”,直接返回-1
if (currentIndex == -1) {
break;
}
}
return currentIndex;
}
}
AutoMergeCode.java
package org.example.utils;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.*;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
/**
* svn合并代码
* 优点:
* 代码分支可以代码中自定义更改路径匹配规则,通用性强;
* 仓库层级较相对方式一更深,更加精确的路径下进行svn操作,会相对较快;
* 使用命令还原代码,且可以删除非版本管控下的文件,还原的干净
* 缺点:
* svn部分操作均通过java执行命令方式进行,需自行更改逻辑或代码
*/
@Slf4j
public class AutoMergeCode {
// SVN仓库地址
private static String SIT_REPOSITORY = "http://****/svn/****/branches/sit";
private static String UAT_REPOSITORY = "http://****/svn/****/test/uat";
// 本地工作副本目录
private static String LOCAL_WORKING_COPY = "/temp/uat_working_copy";
// SVN认证信息
private static final String USERNAME = "****";
private static final String PASSWORD = "****";
public static void merge(String sitVision) {
// 初始化SVNKit库
setupLibrary();
long sitRevision = Long.parseLong(sitVision);
log.info("获取的SIT分支的版本号: {}", sitRevision);
try {
// 执行合并操作
mergeSitToUat(sitRevision);
log.info("合并成功完成!");
} catch (SVNException e) {
log.error("合并过程中发生错误: " + e.getMessage());
e.printStackTrace();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
//还原静态成员变量
SIT_REPOSITORY = "http://****/svn/****P/branches/sit";
UAT_REPOSITORY = "http://****/svn/****/test/uat";
LOCAL_WORKING_COPY = "/temp/uat_working_copy";
log.info("=======================================================================================================");
}
}
private static void setupLibrary() {
// 初始化SVNKit库
SVNRepositoryFactoryImpl.setup();
}
/**
* 合并操作
*
* @param sitRevision sit分支svn版本号
* @throws SVNException
* @throws IOException
* @throws InterruptedException
*/
private static void mergeSitToUat(long sitRevision) throws SVNException, IOException, InterruptedException {
// 创建认证管理器
ISVNAuthenticationManager authManager =
SVNWCUtil.createDefaultAuthenticationManager(USERNAME, PASSWORD.toCharArray());
// 创建SVN客户端管理器 - 使用新的API
SVNClientManager clientManager = SVNClientManager.newInstance(
new DefaultSVNOptions(), authManager);
// 获取SIT版本的日志信息
SVNRepository sitRepository = SVNRepositoryFactory.create(
SVNURL.parseURIEncoded(SIT_REPOSITORY));
sitRepository.setAuthenticationManager(authManager);
// 获取SIT版本的提交信息和作者
/*
* new String[]{""}:指定需要获取日志的路径数组。这里传入空字符串数组{""},表示获取版本库根目录的日志(空字符串在 SVN 中通常代表根路径)。
null:用于过滤日志的修订版本范围(通常是SVNRevisionRange数组)。传入null表示不过滤特定范围,即包含符合条件的所有相关修订版。
sitRevision:日志查询的起始修订版本(包含该版本)。
sitRevision:日志查询的结束修订版本(包含该版本)。
这里起始和结束版本相同,说明是获取单一特定修订版(sitRevision) 的日志。
true:是否递归获取子目录的日志。true表示包含所有子目录的日志,false则只获取指定路径本身的日志。
true:是否获取日志的详细信息。true表示返回的SVNLogEntry会包含完整信息(如作者、提交时间、日志注释、修改的文件列表等);false可能只返回基础信息(如修订版号)。
* */
Collection<SVNLogEntry> logEntries = sitRepository.log(
new String[]{""},
null,
sitRevision,
sitRevision,
true,
true
);
if (logEntries.isEmpty()) {
SVNErrorMessage e = SVNErrorMessage.create(SVNErrorCode.UNKNOWN,
"无法找到版本 " + sitRevision + " 的日志信息");
throw new SVNException(e);
}
SVNLogEntry logEntry = logEntries.iterator().next();
/*
* 修订版号:logEntry.getRevision()
提交作者:logEntry.getAuthor()
提交时间:logEntry.getDate()
提交注释:logEntry.getMessage()
修改的文件列表:logEntry.getChangedPaths()
* */
//sit分支提交的log
String sitLogMessage = logEntry.getMessage().trim();
//sit分支提交的作者
String sitAuthor = logEntry.getAuthor();
//sit分支提交代码列表清单
Map<String, SVNLogEntryPath> changedPaths = logEntry.getChangedPaths();
//代码服务名
String serverName = "【" + (CollectionUtil.newArrayList(changedPaths.keySet())).get(0).trim()
.split("/")[4] + "】";
String server = (CollectionUtil.newArrayList(changedPaths.keySet())).get(0).trim().split("/")[4];
log.info("应用名:{}", server);
//获取分支仓库下具体服务地址,如:/Dubbo/icap-dubbo-cc
// s: /branches/sit/Servers/icap-mbank/test/java/cn/com/common/util/StringUtil.java
String s = (CollectionUtil.newArrayList(changedPaths.keySet())).get(0).trim();
String s1 = s.substring(StringUtils.getNthSlashIndex(s, "/", 3), StringUtils.getNthSlashIndex(s, "/", 5));
if (StrUtil.equals(server, "icap-hf-mbank")) {
SIT_REPOSITORY = SIT_REPOSITORY + s1;
UAT_REPOSITORY = UAT_REPOSITORY + s1.replace("icap-hf-mbank", "icap-mbank-hf");
LOCAL_WORKING_COPY += "/" + server.replace("icap-hf-mbank", "icap-mbank-hf");
} else if (StrUtil.equals(server, "icap-jl-mbank")) {
SIT_REPOSITORY = SIT_REPOSITORY + s1;
UAT_REPOSITORY = UAT_REPOSITORY + s1.replace("icap-jl-mbank", "icap-mbank-jl");
LOCAL_WORKING_COPY += "/" + server.replace("icap-jl-mbank", "icap-mbank-jl");
} else {
SIT_REPOSITORY = SIT_REPOSITORY + s1;
UAT_REPOSITORY = UAT_REPOSITORY + s1;
LOCAL_WORKING_COPY += "/" + server;
}
/* System.out.println("SIT_REPOSITORY " + SIT_REPOSITORY);
System.out.println("UAT_REPOSITORY " + UAT_REPOSITORY);
System.out.println("LOCAL_WORKING_COPY " + LOCAL_WORKING_COPY);*/
File workingCopyDir = new File(LOCAL_WORKING_COPY);
//工作副本检查还原
log.info("检查并还原工作副本...");
//删除非版本化文件及目录
LinuxCommandExecutor.executeCommand(
"svn status | grep '^?' | awk '{print $2}' | xargs rm -rf",
workingCopyDir);
//还原版本化文件
// log.info((LinuxCommandExecutor.executeCommand("svn revert -R .", workingCopyDir))); //命令形式
revertWorkingCopy(clientManager, workingCopyDir);
log.info("更新工作副本...");
// log.info((LinuxCommandExecutor.executeCommand("svn up", workingCopyDir))); //命令形式
long l = updateWorkingCopy(clientManager, workingCopyDir);
log.info("工作副本已更新到版本 {}", l);
/* System.out.println((LinuxCommandExecutor.executeCommand(
"svn status | grep '^?' | awk '{print $2}' | xargs rm -rf",
new File(LOCAL_WORKING_COPY.toString() + "/" + server))));*/
String serverName1 = serverName.replace("【", "[").replace("】", "]");
//遍历代码清单
log.info("要合并的代码清单:");
for (String path : changedPaths.keySet()) {
log.info(path);
/* SVNLogEntryPath svnLogEntryPath = changedPaths.get(path);
//更改类型: 'A' 添加, 'D' 删除, 'M' 修改, 'R' 替换等
System.out.println(svnLogEntryPath.getType());*/
}
// 构建提交消息 例如:【icap-mbank】 新增登录controller xuezhiqian 52158
String commitMessage;
if (sitLogMessage.startsWith(serverName)) {
commitMessage = String.format("%s %s %d",
sitLogMessage, sitAuthor, sitRevision);
} else if (sitLogMessage.startsWith(serverName1)) {
commitMessage = String.format("%s %s %d",
sitLogMessage.replace(serverName1, serverName), sitAuthor, sitRevision);
} else {
commitMessage = String.format("%s %s %s %d",
serverName, sitLogMessage, sitAuthor, sitRevision);
}
log.info("uat分支提交log信息: {}", commitMessage);
log.info("正在合并SIT版本 " + sitRevision + " 到 UAT...");
// 执行合并操作
mergeRepositoryRevision(clientManager, SIT_REPOSITORY, sitRevision, workingCopyDir);
// 检查是否有冲突
SVNStatusClient statusClient = clientManager.getStatusClient();
SVNStatus status = statusClient.doStatus(workingCopyDir, false);
if (status != null && status.getContentsStatus() == SVNStatusType.STATUS_CONFLICTED) {
log.error("警告: 合并过程中发现冲突,请手动解决冲突后重新提交!");
return;
}
// 提交更改
log.info("正在提交更改到UAT...");
SVNCommitClient commitClient = clientManager.getCommitClient();
SVNCommitInfo commitInfo = commitClient.doCommit(
new File[]{workingCopyDir},
false,
commitMessage,
null,
null,
false,
false,
SVNDepth.INFINITY
);
log.info("提交完成! 新版本号: " + commitInfo.getNewRevision());
}
/**
* 还原工作副本中的所有本地修改(版本化内)
*
* @param clientManager SVN 客户端管理器,用于获取操作 SVN 工作副本的客户端对象
* @param workingCopyDir 需要还原的 SVN 工作副本目录(本地文件系统中的路径)
* @throws SVNException 声明可能抛出 SVN 操作相关的异常(如路径不存在、权限不足等)
*/
private static void revertWorkingCopy(SVNClientManager clientManager, File workingCopyDir) throws SVNException {
SVNWCClient wcClient = clientManager.getWCClient();
// 还原整个工作副本,包括所有子目录
/*
* 第一个参数new File[]{workingCopyDir}:需要还原的路径数组。这里传入包含workingCopyDir的数组,表示要还原整个工作副本目录。
第二个参数SVNDepth.INFINITY:还原的深度。INFINITY表示递归还原所有子目录和文件(即不仅还原根目录,还包括所有子级内容)。
第三个参数null:过滤规则(通常用于指定只还原特定类型的文件),null表示无过滤,还原所有内容。
* */
wcClient.doRevert(
new File[]{workingCopyDir},
SVNDepth.INFINITY,
null
);
}
/**
* 更新SVN工作副本到最新版本
*
* @param clientManager SVN客户端管理器
* @param workingCopyDir 工作副本目录
* @return 更新后的修订版本号
* @throws SVNException SVN操作异常
*/
private static long updateWorkingCopy(SVNClientManager clientManager, File workingCopyDir)
throws SVNException {
// 调用重载方法,默认更新到最新版本
return updateWorkingCopy(clientManager, workingCopyDir, SVNRevision.HEAD);
}
/**
* 更新SVN工作副本到指定版本
*
* @param clientManager SVN客户端管理器
* @param workingCopyDir 工作副本目录
* @param targetRevision 目标版本(SVNRevision.HEAD表示最新版本)
* @return 更新后的实际修订版本号
* @throws SVNException SVN操作异常
*/
private static long updateWorkingCopy(SVNClientManager clientManager, File workingCopyDir, SVNRevision targetRevision)
throws SVNException {
// 获取SVN更新客户端
SVNUpdateClient updateClient = clientManager.getUpdateClient();
// 配置更新选项:是否忽略外部项目(如svn:externals定义的依赖)
updateClient.setIgnoreExternals(false);
// 执行更新操作
/*
* 参数说明:
* 1. workingCopyDir:要更新的工作副本目录
* 2. targetRevision:目标版本(HEAD表示最新版本)
* 3. depth:更新深度(INFINITY表示递归更新所有子目录和文件)
* 4. allowUnversionedObstructions:是否允许未版本化的文件存在
* 5. ignoreExternals:是否忽略外部项目
*/
long updatedRevision = updateClient.doUpdate(
workingCopyDir,
targetRevision,
SVNDepth.INFINITY,
false, // 对应原来的allowUnversionedObstructions参数
false // 对应原来的ignoreExternals参数
);
return updatedRevision;
}
/**
* 获取指定版本的所有变更路径
*/
private static SVNLogEntryPath[] getChangedPathsForRevision(SVNRepository repository, long revision)
throws SVNException {
Collection<SVNLogEntry> logEntries = repository.log(
new String[]{""},
null,
revision,
revision,
true,
true
);
if (logEntries.isEmpty()) {
return new SVNLogEntryPath[0];
}
SVNLogEntry logEntry = logEntries.iterator().next();
return logEntry.getChangedPaths().values().toArray(new SVNLogEntryPath[0]);
}
/**
* 将SVN仓库中两个连续版本的差异合并到本地工作副本
*
* @param clientManager SVN客户端管理器
* @param repositoryUrl SVN仓库sit分支URL字符串
* @param targetRevision 目标版本号,(将合并该版本与上一版本的差异):如sit合并到uat,此版本号即为sit版本号
* @param workingCopyDir 本地uat工作副本目录
* @throws SVNException 当SVN操作失败时抛出(如URL无效、版本不存在等)
*/
private static void mergeRepositoryRevision(SVNClientManager clientManager,
String repositoryUrl,
long targetRevision,
File workingCopyDir) throws SVNException {
// 校验目标版本号有效性(至少为1,因为需要比较上一版本)
if (targetRevision < 1) {
throw new IllegalArgumentException("目标版本号必须大于等于1");
}
// 获取差异合并客户端
SVNDiffClient diffClient = clientManager.getDiffClient();
// 执行合并操作:将repositoryUrl仓库中(targetRevision-1)到targetRevision的差异合并到工作副本
diffClient.doMerge(
SVNURL.parseURIEncoded(repositoryUrl), // 源仓库URL
SVNRevision.create(targetRevision - 1), // 源版本(上一版本)
SVNURL.parseURIEncoded(repositoryUrl), // 目标仓库URL(与源仓库相同)
SVNRevision.create(targetRevision), // 目标版本
workingCopyDir, // 合并到的本地工作副本
SVNDepth.INFINITY, // 递归合并所有子目录和文件
true, // 忽略文件祖先关系
false, // 不强制合并(避免覆盖本地修改)
false, // 实际执行合并(非模拟)
false // 应用变更到文件(非仅记录)
);
}
}
Main.java
package org.example;
import lombok.extern.slf4j.Slf4j;
import org.example.utils.AutoMergeCode;
import org.example.utils.SVNMergeTool;
import java.io.IOException;
@Slf4j
public class Main {
public static void main(String[] args) throws IOException {
String arg = args[0];
for (String sitRevision : arg.trim().split("\n")) {
AutoMergeCode.merge(sitRevision.trim());
}
}
}
方法三:(强烈推荐)
优点:在方式一上进行更改增强,合并层级更加精确,提高了速度
SVNMergeCodeTool.java
package org.example.utils;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.*;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
/**
* svn合并代码
* 优点:
* svn所有操作均使用官方提供的svnkit库,官方保障
* 缺点:
* 合代码分支固定,无法根据路径进行更改;
* 仓库层级较浅,如果代码库代码量超级庞大,则svn操作会很慢
*/
@Slf4j
public class SVNMergeCodeTool {
// SVN仓库地址
private static String SIT_REPOSITORY = "http://192.168.206.135/svn/ICAP/branches/sit";
private static String UAT_REPOSITORY = "http://192.168.206.135/svn/ICAP/test/uat";
// 本地工作副本目录
private static String LOCAL_WORKING_COPY = "/temp/UAT";
// SVN认证信息
private static final String USERNAME = "lwx1091923";
private static final String PASSWORD = "lmy1024++";
public static void merge(String sitVision) {
// 初始化SVNKit库
setupLibrary();
long sitRevision = Long.parseLong(sitVision);
log.info("获取的SIT分支的版本号: {}", sitRevision);
try {
// 执行合并操作
mergeSitToUat(sitRevision);
log.info("合并成功完成!");
} catch (SVNException e) {
log.error("合并过程中发生错误: " + e.getMessage());
e.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//还原静态成员变量
SIT_REPOSITORY = "http://192.168.206.135/svn/ICAP/branches/sit";
UAT_REPOSITORY = "http://192.168.206.135/svn/ICAP/test/uat";
LOCAL_WORKING_COPY = "/temp/UAT";
log.info("=======================================================================================================");
}
}
private static void setupLibrary() {
// 初始化SVNKit库
SVNRepositoryFactoryImpl.setup();
// 不再需要调用 SVNClientManager.setup();
}
private static void mergeSitToUat(long sitRevision) throws SVNException, IOException {
// 创建认证管理器
ISVNAuthenticationManager authManager =
SVNWCUtil.createDefaultAuthenticationManager(USERNAME, PASSWORD.toCharArray());
// 创建SVN客户端管理器 - 使用新的API
SVNClientManager clientManager = SVNClientManager.newInstance(
new DefaultSVNOptions(), authManager);
// 获取UAT工作副本
SVNUpdateClient updateClient = clientManager.getUpdateClient();
updateClient.setIgnoreExternals(false);
File workingCopyDirRoot = new File(LOCAL_WORKING_COPY);
// 获取SIT版本的日志信息
SVNRepository sitRepository = SVNRepositoryFactory.create(
SVNURL.parseURIEncoded(SIT_REPOSITORY));
sitRepository.setAuthenticationManager(authManager);
// 获取SIT版本的提交信息和作者
Collection<SVNLogEntry> logEntries = sitRepository.log(
new String[]{""}, // 空数组表示根目录
null,
sitRevision,
sitRevision,
true,
true
);
if (logEntries.isEmpty()) {
SVNErrorMessage e = SVNErrorMessage.create(SVNErrorCode.UNKNOWN,
"无法找到版本 " + sitRevision + " 的日志信息");
throw new SVNException(e);
}
SVNLogEntry logEntry = logEntries.iterator().next();
//sit分支提交的log
String sitLogMessage = logEntry.getMessage().trim();
//sit分支提交的作者
String sitAuthor = logEntry.getAuthor();
//sit分支提交代码列表清单
Map<String, SVNLogEntryPath> changedPaths = logEntry.getChangedPaths();
//代码服务名
String server = (CollectionUtil.newArrayList(changedPaths.keySet())).get(0).trim().split("/")[4];
String serverName = "【" + server + "】";
String serverName1 = serverName.replace("【", "[").replace("】", "]");
//遍历代码清单
log.info("要合并的代码清单:");
for (String path : changedPaths.keySet()) {
log.info(path);
/* SVNLogEntryPath svnLogEntryPath = changedPaths.get(path);
//更改类型: 'A' 添加, 'D' 删除, 'M' 修改, 'R' 替换等
System.out.println(svnLogEntryPath.getType());*/
}
// 构建提交消息 例如:【icap-mbank】 新增登录controller xuezhiqian 52158
String commitMessage;
if (sitLogMessage.startsWith(serverName)) {
commitMessage = String.format("%s %s %d",
sitLogMessage, sitAuthor, sitRevision);
} else if (sitLogMessage.startsWith(serverName1)) {
commitMessage = String.format("%s %s %d",
sitLogMessage.replace(serverName1, serverName), sitAuthor, sitRevision);
} else {
commitMessage = String.format("%s %s %s %d",
serverName, sitLogMessage, sitAuthor, sitRevision);
}
log.info("uat分支提交log信息:{}", commitMessage);
//工作副本
File workingCopyDir = null;
// 检查工作副本是否存在,如果不存在则checkout
if (!workingCopyDirRoot.exists() || !SVNWCUtil.isWorkingCopyRoot(workingCopyDirRoot)) {
log.info("正在检出UAT工作副本...");
updateClient.doCheckout(
SVNURL.parseURIEncoded(UAT_REPOSITORY),
workingCopyDirRoot,
SVNRevision.HEAD,
SVNRevision.HEAD,
SVNDepth.INFINITY,
false
);
} else {
//根据要合并的代码服务名重新定义工作副本
//获取分支仓库下具体服务地址,如:/Dubbo/icap-dubbo-cc
// s: /branches/sit/Servers/icap-mbank/test/java/cn/com/common/util/StringUtil.java
String s = (CollectionUtil.newArrayList(changedPaths.keySet())).get(0).trim();
String s1 = s.substring(StringUtils.getNthSlashIndex(s, "/", 3), StringUtils.getNthSlashIndex(s, "/", 5));
if (StrUtil.equals(server, "icap-hf-mbank")) {
SIT_REPOSITORY = SIT_REPOSITORY + s1;
UAT_REPOSITORY = UAT_REPOSITORY + s1.replace("icap-hf-mbank", "icap-mbank-hf");
LOCAL_WORKING_COPY += "/" + s1.replace("icap-hf-mbank", "icap-mbank-hf");
workingCopyDir = new File(LOCAL_WORKING_COPY);
} else if (StrUtil.equals(server, "icap-jl-mbank")) {
SIT_REPOSITORY = SIT_REPOSITORY + s1;
UAT_REPOSITORY = UAT_REPOSITORY + s1.replace("icap-jl-mbank", "icap-mbank-jl");
LOCAL_WORKING_COPY += "/" + s1.replace("icap-jl-mbank", "icap-mbank-jl");
workingCopyDir = new File(LOCAL_WORKING_COPY);
} else {
SIT_REPOSITORY = SIT_REPOSITORY + s1;
UAT_REPOSITORY = UAT_REPOSITORY + s1;
LOCAL_WORKING_COPY += "/" + s1;
workingCopyDir = new File(LOCAL_WORKING_COPY);
}
log.info("工作副本:{}", workingCopyDir);
// 在更新前先执行还原操作,确保工作副本干净
log.info("正在还原UAT工作副本...");
revertWorkingCopy(clientManager, workingCopyDir);
//删除非版本化文件及目录
log.info("正在删除工作副本非版本化文件及目录...");
try {
LinuxCommandExecutor.executeCommand(
"svn status | grep '^?' | awk '{print $2}' | xargs rm -rf",
workingCopyDir);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("正在更新UAT工作副本...");
updateClient.doUpdate(
workingCopyDir,
SVNRevision.HEAD,
SVNDepth.INFINITY,
false,
false
);
}
log.info("正在合并SIT版本 " + sitRevision + " 到 UAT...");
// 执行合并操作
SVNDiffClient diffClient = clientManager.getDiffClient();
diffClient.doMerge(
SVNURL.parseURIEncoded(SIT_REPOSITORY),
SVNRevision.create(sitRevision - 1),
SVNURL.parseURIEncoded(SIT_REPOSITORY),
SVNRevision.create(sitRevision),
workingCopyDir,
SVNDepth.INFINITY,
true,
false,
false,
false
);
// 检查是否有冲突
SVNStatusClient statusClient = clientManager.getStatusClient();
SVNStatus status = statusClient.doStatus(workingCopyDir, false);
if (status != null && status.getContentsStatus() == SVNStatusType.STATUS_CONFLICTED) {
log.error("警告: 合并过程中发现冲突,请手动解决冲突后重新提交!");
return;
}
// 提交更改
log.info("正在提交更改到UAT...");
SVNCommitClient commitClient = clientManager.getCommitClient();
SVNCommitInfo commitInfo = commitClient.doCommit(
new File[]{workingCopyDir},
false,
commitMessage,
null,
null,
false,
false,
SVNDepth.INFINITY
);
log.info("提交完成! 新版本号: " + commitInfo.getNewRevision());
}
/**
* 还原工作副本中的所有本地修改(版本化内)
*
* @param clientManager SVN 客户端管理器,用于获取操作 SVN 工作副本的客户端对象
* @param workingCopyDir 需要还原的 SVN 工作副本目录(本地文件系统中的路径)
* @throws SVNException 声明可能抛出 SVN 操作相关的异常(如路径不存在、权限不足等)
*/
private static void revertWorkingCopy(SVNClientManager clientManager, File workingCopyDir) throws SVNException {
SVNWCClient wcClient = clientManager.getWCClient();
// 还原整个工作副本,包括所有子目录
/*
* 第一个参数new File[]{workingCopyDir}:需要还原的路径数组。这里传入包含workingCopyDir的数组,表示要还原整个工作副本目录。
第二个参数SVNDepth.INFINITY:还原的深度。INFINITY表示递归还原所有子目录和文件(即不仅还原根目录,还包括所有子级内容)。
第三个参数null:过滤规则(通常用于指定只还原特定类型的文件),null表示无过滤,还原所有内容。
* */
wcClient.doRevert(
new File[]{workingCopyDir},
SVNDepth.INFINITY,
null
);
}
/**
* 获取指定版本的所有变更路径
*/
private static SVNLogEntryPath[] getChangedPathsForRevision(SVNRepository repository, long revision)
throws SVNException {
Collection<SVNLogEntry> logEntries = repository.log(
new String[]{""},
null,
revision,
revision,
true,
true
);
if (logEntries.isEmpty()) {
return new SVNLogEntryPath[0];
}
SVNLogEntry logEntry = logEntries.iterator().next();
return logEntry.getChangedPaths().values().toArray(new SVNLogEntryPath[0]);
}
/**
* 合并单个文件
*/
private static void mergeSingleFile(
SVNClientManager clientManager,
File workingCopyDir,
String sitPath,
String uatPath,
long sitRevision) throws SVNException {
SVNDiffClient diffClient = clientManager.getDiffClient();
try {
// 获取SIT文件的URL
SVNURL sitFileUrl = SVNURL.parseURIEncoded(SIT_REPOSITORY + sitPath);
// 获取UAT工作副本中的文件路径
File uatFile = new File(workingCopyDir, uatPath);
// 确保目标目录存在
uatFile.getParentFile().mkdirs();
// 执行合并
diffClient.doMerge(
sitFileUrl,
SVNRevision.create(sitRevision - 1),
sitFileUrl,
SVNRevision.create(sitRevision),
uatFile,
SVNDepth.EMPTY,
true,
false,
false,
false
);
} catch (SVNException e) {
log.error("合并文件失败: " + sitPath + " -> " + uatPath + ": " + e.getMessage());
// 可以选择继续处理其他文件
}
}
}
更多推荐




所有评论(0)