前言
实际上项目团队已经在推Git了,但是大多数老的项目依然在用SVN管理,现在需要大力推行Jenkins自动化编译上传仓库,但是看了下SVN带的hook也就是调用系统脚本的功能,也只能对一个repository设置hook,无法直接得到项目名。考虑再三,决定拿Java写一个webhook包,在服务器上设置SVN的Post-commit Hook来调用,用了一段时间还比较稳定(现有环境:Windows Server/ VisualSVN,其他部署环境也可尝试)。
代码
整个工具尽可能简单,由一个pom.xml 和一个 java代码文件组成,配置和代码加起来大概200行的样子。
- 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>cn.hook</groupId>
<artifactId>svn-hook</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>cn.hook.SVNHook</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
</project>
- SVNHook.java
package cn.hook;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
public class SVNHook{
private static File logFile = null;
public static void main(String[] args){
if(args.length < 1) {
printLog("Missing config");
return;
}
String configPath = args[0];
File configFile = new File(configPath);
Properties config;
if(!configFile.exists()) {
printLog("Config not exists:"+configPath);
return;
}else {
try {
config = initProp(configFile);
} catch (IOException e) {
printLog(e);
return;
}
String logPath = config.getProperty("log");
logFile = new File(logPath);
}
if(args.length < 3) {
printLog("Missing parameter:"+StringUtils.join(args, ","));
return;
}
String repoPath = args[1];
String revision = args[2];
File repoDir = new File(repoPath);
if(!repoDir.exists()) {
printLog("Repo not exists:"+repoPath);
return;
}
printLog("Call!Repo path:"+repoPath+",Revision:"+revision);
try {
String project = getChangeProject(repoPath, revision);
if(project !=null) {
String repoName = repoDir.getName();
printLog("Dirs-changed!Repo:"+repoName+",Project:"+project);
String filter = config.getProperty("filter");
boolean isMatch = false;
if(StringUtils.isNotBlank(filter)) {
String[] matchStrArr = filter.split(",");
for(String matchStr : matchStrArr) {
isMatch = FilenameUtils.wildcardMatch(project, matchStr, IOCase.SENSITIVE);
if(isMatch) break;
}
}
if(isMatch) {
String requestURL = config.getProperty("request");
if(StringUtils.isNotBlank(requestURL)) {
String urlString = StringUtils.replaceEach(requestURL,
new String[]{"{group}","{project}"},
new String[] { repoName , project});
request(urlString);
}else {
printLog("Ignore due to empty request url!");
}
}else {
printLog("Ignore because not match filter!");
}
}
}catch(IOException e) {
printLog(e);
}
}
private static void request(String urlString) throws IOException {
printLog("Request:"+urlString);
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("GET");
connection.connect();
int responseCode = connection.getResponseCode();
InputStream inputStream = connection.getInputStream();
String response = IOUtils.toString(inputStream,"utf8");
if(StringUtils.isNotBlank(response)) {
printLog(String.format("Response(%d):%s", responseCode,response));
}else {
printLog(String.format("Response(%d)!", responseCode));
}
connection.disconnect();
}
private static Properties initProp(File configFile) throws IOException {
Properties prop = new Properties();
InputStream is = new FileInputStream(configFile);
prop.load(is);
return prop;
}
private static String getChangeProject(String repoPath,String revision) throws IOException {
ProcessBuilder builder = new ProcessBuilder();
builder.command("svnlook","dirs-changed",repoPath,"-r",revision);
Process process = builder.start();
InputStream is = process.getInputStream();
List<String> lines = IOUtils.readLines(is, "gbk");
if(lines.size() > 0) {
String headLine = lines.get(0);
String projectName = headLine.split("/")[0];
return projectName;
}
return null;
}
private static void printLog(Throwable e){
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
String msg=sw.toString();
printLog(msg);
}
private static void printLog(String log) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String outLog = String.format("[%s] %s\r\n", format.format(new Date()),log);
if(logFile !=null) {
if(!logFile.canWrite()) {
System.out.println("Log file can not write:"+logFile.getAbsolutePath());
System.out.print(outLog);
return;
}
try {
FileUtils.write(logFile, outLog, "utf8",true);
} catch (IOException e) {
e.printStackTrace();
}
}else {
System.out.print(outLog);
}
}
}
打包、部署、使用
- 打包:mvn clean package
- 部署:取出编译输出路径下的svn-hook-0.0.1-SNAPSHOT-jar-with-dependencies.jar,简单改名成svn-hook.jar,放到服务器上的目录(这里是:D:\svn-hook),另外在该目录下放一个配置文件config.properties:
log=D:/svn-hook/hook.log
filter=*-lib
request=http://192.168.1.1/job/my-job/buildWithParameters?token=mytoken&group={group}&project={project}
此配置文件中:
- log:日志文件,即每次有新提交调用过来这里会输出日志。
- filter:一个项目过滤器,逗号分隔,可使用通配符,只有符合此过滤规则的项目才会发起远程调用。这里*-lib即代表-lib结尾的项目,详细实现即代码中的FilenameUtils.wildcardMatch 调用。
- request:一个请求地址,这里指向一个Jenkins远程调用地址,其中{group}和{project}代表可动态替换的参数,请求时会替换成repository名称和项目名称,这样在Jenkins构建时就知道了哪个SVN项目有最新提交。
- 使用:在SVN服务上创建一个Post-commit Hook,调用脚本内容:java -jar D:\svn-hook\svn-hook.jar D:\svn-hook\config.properties %1 %2
总结说明
本文描述的方法解决以下问题:
- SVN服务的hook功能简单,只提供脚本调用,不同平台Windows/Linux还需要对应平台脚本支持,使用java编写的工具包可跨平台使用且语言功能足以实现更灵活的webhook
- SVN服务的hook不会传递具体提交项目,此工具根据传递的参数调用svnlook获取具体提交项目来执行远程调用,且可灵活过滤使指定的项目才会调用