1、SVNKit介绍
Java与SVN代码管理平台之间的交互主要可以通过SVNKit实现的,SVNKit是一个纯Java的客户端库,用于在Java应用程序中与Subversion版本控制系统(SVN)一起工作。SVNKit实现了所有Subversion的功能,并提供了API来处理Subversion工作副本,访问和操作Subversion存储库。这个库有两个层次的API:高级层用于管理工作拷贝,类似于使用Subversion命令行客户端;低级层类似Subversion仓库访问层,类似于直接在Subversion仓库上工作的驱动器。
2、具体实现案例
需求
生成一系列结构相同的项目代码,将这些项目的代码推送至一个指定的 SVN 仓库,每个项目独占一个分支。
问题
1.SVN不支持远程仓库和远程分支的创建。因此,SVN的仓库地址必须预先存在,否则无法进行服务代码的推送。
2.SVNKit不支持在服务器上创建文件夹未提交的情况下,在文件夹下上传文件。即无法在与svn服务器的一次连接中同时创建文件夹和遍历上传文件,,如果要根据需要上传的文件夹数量多次连接,会大大降低上传效率,代码发布时间会极其漫长。
解决思路
1.虽然SVN不支持创建远程仓库和远程分支,但作为一种集中式版本控制系统,SVN通过复制trunk(主分支)下的文件到branches/(新分支名称)目录下来实现分支的创建。这一过程中,不仅复制了文件信息,还保留了版本历史关联元数据信息。基于这一特性,可以在branches目录下远程创建与服务名称相对应的文件夹,然后将各个服务下的代码上传到相应文件夹中。这样,就可以在一个SVN仓库中管理多个服务,从而满足多服务管理的需求。
2.判断要上传的目录下面有没有不为空的文件夹,如果有就整体打成zip压缩包上传。
代码实现
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNUpdateClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
/**
* @author zhouyu
* @date 2024/3/5 17:47
**/
public class SvnKitUtils {
private static SVNRepository svnRepository;
private static final long REVISION_NO = -1; //指定版本号为最新版本
/**
* 向远程仓库推送代码
*
* @param svnAccount
* @param serverName
* @param path
*/
public static void pushCodeToSVN(Account svnAccount, String serverName, String path) throws Exception {
File file = new File(path);
//源码本地路径
Path sourcePath = file.toPath();
if (!Files.exists(sourcePath)) {
throw new IllegalArgumentException("源码目录不存在!目录地址:" + sourcePath);
}
String url = svnAccount.getRepoUrl();
//账号及仓库路径判断
SvnKitUtils svn = new SvnKitUtils(url, svnAccount.getUsername(), svnAccount.getPassword());
//1.删除已存在的文件夹(已发布即已存在须删除)
try {
ISVNEditor editor = svnRepository.getCommitEditor("删除文件夹:" + serverName, null, true, null);
deleteFile(editor, serverName, REVISION_NO);
//2.新建文件夹
uploadFolder(svnAccount, serverName, sourcePath, url);
} catch (Exception e) {
//删除失败表示当前服务未发布过,需创建新目录
new SvnKitUtils(svnAccount.getRepoUrl(), svnAccount.getUsername(), svnAccount.getPassword());
//2.新建文件夹
uploadFolder(svnAccount, serverName, sourcePath, url);
}
}
private static void uploadFolder(GitAccount svnAccount, String serverName, Path sourcePath,
String url) throws Exception {
ISVNEditor editor1 = svnRepository.getCommitEditor("新建文件夹:" + serverName, null, true, null);
addDir(editor1, REVISION_NO, "", serverName);
url = url + "/" + serverName;
new SvnKitUtils(url, svnAccount.getUsername(), svnAccount.getPassword());
ISVNEditor editor2 = svnRepository.getCommitEditor("新建文件夹:service", null, true, null);
addDir(editor2, REVISION_NO, "", "service");
//3.文件上传
url = url + "/service";
SvnKitUtils svn1 = new SvnKitUtils(url, svnAccount.getUsername(), svnAccount.getPassword());
svn1.commitFolder(sourcePath + "/service", REVISION_NO, "");
}
/**
* 验证登录
*
* @param url svn仓库地址
* @param username 用户名称
* @param password 密码
*/
public SvnKitUtils(String url, String username, String password) {
try {
DAVRepositoryFactory.setup();
SVNRepositoryFactoryImpl.setup();
FSRepositoryFactory.setup();
char[] pwd = password.toCharArray();
svnRepository = SVNRepositoryFactory.create(SVNURL.parseURIEncoded(url));
ISVNAuthenticationManager isvnAuthenticationManager = SVNWCUtil.createDefaultAuthenticationManager(username,
pwd);
svnRepository.setAuthenticationManager(isvnAuthenticationManager);
svnRepository.checkPath("", -1);
} catch (SVNException e) {
throw new RuntimeException("仓库路径错误或账号密码错误");
}
}
/**
* 新增文件
*
* @param revisionNo
* @param dirPath 文件路径
* @param fileName 文件名称
* @param data
* @return
* @throws SVNException
*/
private static SVNCommitInfo addFile(ISVNEditor editor, long revisionNo, String dirPath, String fileName,
byte[] data) throws SVNException {
editor.openRoot(revisionNo);
editor.openDir(dirPath, revisionNo);
editor.addFile(fileName, null, revisionNo);
return getSvnCommitInfo(editor, fileName, data);
}
/**
* 新建文件夹
*
* @param editor
* @param revisionNo
* @param dirPath
* @param fileDirName
* @return
* @throws SVNException
*/
@SuppressWarnings("unused")
private static SVNCommitInfo addDir(ISVNEditor editor, long revisionNo, String dirPath,
String fileDirName) throws SVNException {
editor.openRoot(revisionNo);
editor.openDir(dirPath, revisionNo);
editor.addDir(fileDirName, null, revisionNo);
editor.closeDir();
editor.closeDir();
return editor.closeEdit();
}
/**
* 修改文件
*
* @param dirPath 文件路径 /svn
* @param fileName 文件名称
* @param newData
* @return
* @throws SVNException
*/
private static SVNCommitInfo modifyFile(ISVNEditor editor, String dirPath, String fileName,
byte[] newData) throws SVNException {
editor.openRoot(-1);
editor.openDir(dirPath, -1);
editor.openFile(fileName, -1);
return getSvnCommitInfo(editor, fileName, newData);
}
private static SVNCommitInfo getSvnCommitInfo(ISVNEditor editor, String fileName,
byte[] newData) throws SVNException {
editor.applyTextDelta(fileName, null);
SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
String checksum = deltaGenerator.sendDelta(fileName, new ByteArrayInputStream(newData), editor, true);
editor.closeFile(fileName, checksum);
editor.closeDir();
editor.closeDir();
return editor.closeEdit();
}
/**
* 删除文件
*
* @param editor
* @param dirPath 要删除的文件路径
* @param revisionNo
* @return
* @throws SVNException
*/
private static SVNCommitInfo deleteFile(ISVNEditor editor, String dirPath, long revisionNo) throws Exception {
// 进入Root节点
editor.openRoot(revisionNo);
//删除文件
editor.deleteEntry(dirPath, revisionNo);
//操作完成要关闭编辑器,并返回操作结果
return editor.closeEdit();
}
/**
* 多文件上传
*
* @param localFolderPath
* @param revisionNo
* @param svnFolderPath
* @throws Exception
*/
public void commitFolder(String localFolderPath, long revisionNo, String svnFolderPath) throws Exception {
File folder = new File(localFolderPath);
if (folder.exists() && folder.isDirectory()) {
Path startpath = Paths.get(localFolderPath);
boolean flag = hasNonEmptySubfolder(startpath);
if (!flag) {
ISVNEditor editor = svnRepository.getCommitEditor("文件上传", null, true, null);
editor.openRoot(revisionNo);
editor.openDir(svnFolderPath, revisionNo);
for (File file : Objects.requireNonNull(folder.listFiles())) {
if (file.isFile()) {
uploadFile(revisionNo, editor, file);
}
}
editor.closeDir();
editor.closeDir();
editor.closeEdit();
} else {
Path zipPath = startpath.resolveSibling(startpath.getFileName().toString() + ".zip");
zipDirectory(startpath, zipPath);
if (Files.exists(zipPath)) {
File file = zipPath.toFile();
uploadFileToSVN(revisionNo, svnFolderPath, file);
}
}
}
}
/**
* 文件添加进svn
*
* @param revisionNo
* @param editor
* @param file
* @throws SVNException
*/
private void uploadFile(long revisionNo, ISVNEditor editor, File file) throws Exception {
byte[] data = readFileToByteArray(file);
editor.addFile(file.getName(), null, revisionNo);
editor.applyTextDelta(file.getName(), null);
SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
String checksum = deltaGenerator.sendDelta(file.getName(), new ByteArrayInputStream(data), editor, true);
editor.closeFile(file.getName(), checksum);
}
/**
* 文件上传至svn
*
* @param revisionNo
* @param svnFolderPath
* @param file
* @throws Exception
*/
private void uploadFileToSVN(long revisionNo, String svnFolderPath, File file) throws Exception {
ISVNEditor editor = svnRepository.getCommitEditor("文件上传:" + file.getName(), null, true, null);
editor.openRoot(revisionNo);
editor.openDir(svnFolderPath, revisionNo);
uploadFile(revisionNo, editor, file);
editor.closeDir();
editor.closeDir();
editor.closeEdit();
}
/**
* 文件压缩
*
* @param directoryToZip
* @param zipFilePath
* @throws IOException
*/
public static void zipDirectory(Path directoryToZip, Path zipFilePath) throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(zipFilePath)))) {
zipDirectoryRecursive(directoryToZip, directoryToZip, zos);
}
}
/**
* 文件压缩
*
* @param source
* @param sourceBasePath
* @param zos
* @throws IOException
*/
private static void zipDirectoryRecursive(Path source, Path sourceBasePath,
ZipOutputStream zos) throws IOException {
Path relativePath = sourceBasePath.relativize(source);
if (Files.isDirectory(source)) {
// 如果是目录,则添加目录到ZIP(作为文件夹)
ZipEntry dirEntry = new ZipEntry(relativePath + "/");
zos.putNextEntry(dirEntry);
zos.closeEntry();
// 递归遍历子目录
try (Stream
paths.forEach(child -> {
try {
zipDirectoryRecursive(child, sourceBasePath, zos);
} catch (IOException e) {
e.printStackTrace();
}
});
}
} else {
// 如果是文件,则添加到ZIP
ZipEntry fileEntry = new ZipEntry(relativePath.toString());
zos.putNextEntry(fileEntry);
try (InputStream in = Files.newInputStream(source)) {
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}
zos.closeEntry();
}
}
/**
* 因为暂时无法边在svn服务器上创建文件夹,边遍历上传文件,
* 所以判断要上传的目录下面有没有不为空的文件夹,如果有就整体打成zip压缩包上传
*
* @param folderPath
* @return
*/
public static boolean hasNonEmptySubfolder(Path folderPath) {
try (Stream
return subfolderStream
// 只保留子文件夹
.filter(Files::isDirectory)
.anyMatch(subfolder -> {
try {
// 获取子文件夹中的文件流
try (Stream
// 如果子文件夹中有文件,则返回true
return fileStream.findAny().isPresent();
}
} catch (IOException e) {
// 如果无法读取子文件夹内容,则抛出异常
throw new RuntimeException("Failed to list files in subfolder", e);
}
});
} catch (IOException e) {
// 如果无法读取文件夹内容,则抛出异常
throw new RuntimeException("Failed to list subfolders", e);
}
}
private byte[] readFileToByteArray(File file) throws Exception {
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
// 使用一个固定大小的缓冲区
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
return bos.toByteArray();
}
}
/**
* 代码拉取
*
* @param url svn地址
* @param local 本地路径
* @param username
* @param password
*/
private static void checkout(String url, String local, String username, String password) {
SVNClientManager ourClientManager;
//初始化支持svn://协议的库。 必须先执行此操作。
SVNRepositoryFactoryImpl.setup();
//相关变量赋值
SVNURL repositoryURL = null;
try {
repositoryURL = SVNURL.parseURIEncoded(url + "/trunk");
} catch (SVNException e) {
System.out.println(e.getMessage());
System.exit(1);
}
DefaultSVNOptions options = SVNWCUtil.createDefaultOptions(true);
//实例化客户端管理类
ourClientManager = SVNClientManager.newInstance(options, username, password);
//要把版本库的内容check out到的目录
File desFile = new File(local);
//通过客户端管理类获得updateClient类的实例。
SVNUpdateClient updateClient = ourClientManager.getUpdateClient();
updateClient.setIgnoreExternals(false);
try {
//返回工作版本号 -1表示最新
long workingVersion = updateClient.doCheckout(repositoryURL, desFile, SVNRevision.HEAD,
SVNRevision.parse("-1"), SVNDepth.INFINITY, true);
System.out.println("把版本:" + workingVersion + " check out 到目录:" + local + "中");
} catch (SVNException e) {
System.out.println("检出代码出错" + e);
System.exit(1);
}
}
}
public class Account {
@NotBlank(message = "仓库URL不能为空")
@ApiModelProperty("仓库URL")
private String repoUrl;
@NotBlank(message = "用户名不能为空")
@ApiModelProperty("用户名")
private String username;
@NotBlank(message = "密码不能为空")
@ApiModelProperty("密码")
private String password;
}
/**
* 生成代码
*
* @param branchName
* @return
* @throws Exception
*/
private static File genCode(String branchName) throws Exception {
File codeDir = Files.createTempDirectory("svn-source-dir-").toFile();
File svcDir = new File(dirPath, "service");
String codeDirAbsPath = svcDir.getAbsolutePath();
// 生成文件
File cppFile = new File(codeDirAbsPath.concat(File.separator).concat(branchName.concat(".cpp")));
String cppContent = "Cpp Code for " + branchName + "\r\n Write By Code svnTest ";
Files.write(cppFile.toPath(), cppContent.getBytes());
// 生成文件
File hFile = new File(codeDirAbsPath.concat(File.separator).concat(branchName.concat(".h")));
String hContent = "Header code for " + branchName + "\r\nWrite By Code svnTest ";
Files.write(hFile.toPath(), hContent.getBytes());
return codeDir;
}
public static void main(String[] args) throws Exception {
List
String svnUrl = "svn仓库地址";
String username = "root";
String password = "123456";
for (String branchName : branches) {
GitAccount gitAccount=new GitAccount(svnUrl,username,password);
String path=genCode(branchName).getAbsolutePath()
pushCodeToSVN(gitAccount,branchName,path);
}
}
3、附
引入SVNkit: