简要说一下:开发这个Jenkins插件的初衷是解决公司在代码管理上遇到的问题;现状是:我目前所在的这家公司技术上真的是老古董的那种,代码管理水平真的很一般(各种夹带...),所以不得已才需要做这个插件协助开发区diff文件甚至到单行代码的变更情况(目前由于技术水平有限,只diff出文件的变化;我觉得如果diff到具体谋行的变更最难的地方是数据要怎么展示、这个是一个难点... 数据的获取可以通过git命令抓取)
//如下:为diff两个远程分支的文件变更
git diff origin/Release_v1.0 origin/master --stat
diff 之后的结果如下,+ 表示 master 分支相对 Release_v1.0 分支增加的 - 表示 master 分支相对 Release_v1.0 分支删除的代码;两个分支没有任何变更的文件不会展示出来
注意:
1:上述为 diff 两个远程分支,如果要 diff 本地分支只有去掉"origin/"即可
2:Git 的账号、密码、仓库 URL 要设置(如果是在拉取的代码的工作目录下这一步可以省略)
//如下:为diff两个远程分支的src/main/java/Core/Filter/xxx.java文件的变更
git diff origin/Release_v1.0 origin/master src/main/java/Core/Filter/xxx.java
//如下 diff两个远程分支src/main/java/Core/Filter文件夹下所有的文件变更情况
git diff origin/Release_v1.0 origin/master src/main/java/Core/Filter
diff 之后的文件如下,代码行全面标记"-"表示 master 分支相对 Release_v1.0 分支减少的行;"+"表示 master 分支相对 Release_v1.0 分支增加的代码行
配置 maven 仓库的 settings.xml,下面这端匹配 copy 到 maven 的 settings.xml 中
<settings>
<pluginGroups>
<pluginGroup>org.jenkins-ci.tools</pluginGroup>
</pluginGroups>
<profiles>
<!-- Give access to Jenkins plugins -->
<profile>
<id>jenkins</id>
<activation>
<activeByDefault>true</activeByDefault> <!-- change this to false, if you don't like to have it on per default -->
</activation>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
<mirrors>
<mirror>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
<mirrorOf>m.g.o-public</mirrorOf>
</mirror>
</mirrors>
</settings>
在指定目录下执行下列 maven 命令,会自动生成一个 maven 项目,然后倒入 Eclipse 或者 IDEA(推荐)
//your.gound.id 例如:com.jenkins.plugins your.plugin.id 例如:plugins
mvn -U org.jenkins-ci.tools:maven-hpi-plugin:create -DgroupId={your.gound.id} -DartifactId={your.plugin.id}
代码结构如下,红框是自动生成的,pom.xml 也不用修改(如果你需要其他依赖可以直接再导入)
本地运行,访问http://localhost:8080/jenkinssay会在"构建"下多一个" hello"的插件
//本地调试,默认启动8080端口
mvn hpi:run
//打包 会在target目录下生成一个xx.hpi的文件,我们可以使用这个hpi文件在jenkins插件管理中进行本地安装
mvn clean package
至此 我们已经完成了一个 Jenkins 插件开发的 Hello Word,下面我们开始实现 Jenkins 插件的代码 diff 功能
import xxx.Jenkins.Plugins.Message.WeChartMess;
import finchina.Jenkins.Plugins.Utils.Excution;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractProject;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.FormValidation;
import jenkins.tasks.SimpleBuildStep;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 源码diff工具
*/
public class SourceDiffBuilder_Git extends Builder implements SimpleBuildStep {
//老分支 非null
private final String consult_Branch;
//新分支 非null
private final String tag_Branch;
//消息通知
private final String weChartUrl;
private final String atUsers;
/**
* 高级功能部分
* */
//扫描指定目录下的文件
private final String tagPaths;
//屏蔽指定条件的文件或者数据(*.xml:以.xml结尾的;*xml*:文件名称中包含xml的)
private final String blockFiles;
@DataBoundConstructor
public SourceDiffBuilder_Git(String consult_Branch, String tag_Branch, String weChartUrl, String atUsers , String tagPaths,
String blockFiles) {
this.consult_Branch = consult_Branch;
this.tag_Branch = tag_Branch;
this.atUsers = atUsers;
this.weChartUrl = weChartUrl;
this.tagPaths = tagPaths;
this.blockFiles = blockFiles;
}
@Override
public String toString() {
return "SourceDiffBuilder_Git{" +
"consult_Branch='" + consult_Branch + '\'' +
", tag_Branch='" + tag_Branch + '\'' +
", weChartUrl='" + weChartUrl + '\'' +
", atUsers='" + atUsers + '\'' +
", tagPaths='" + tagPaths + '\'' +
", blockFiles='" + blockFiles + '\'' +
'}';
}
public String getConsult_Branch() {
return consult_Branch;
}
public String getTag_Branch() {
return tag_Branch;
}
public String getWeChartUrl() {
return weChartUrl;
}
public String getAtUsers() {
return atUsers;
}
public String getTagPaths() {
return tagPaths;
}
public String getBlockFiles() {
return blockFiles;
}
@Override
public void perform(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
listener.getLogger().println("get diff parmaters show: "+this.toString());
/**
* demo
*
* 输出:
* getPreviousBuildUrl = job/demo/8/
* build.getUrl = job/demo/9/
* build.getId = 2020-07-16_11-12-24
* build.getDisplayName = #9
* build.getEnvironment(listener).expand("testEnv") = testEnv
* build.getEnvironment(listener).get("testEnv") = 这是测试验证
* build.getEnvironment(listener).get("WORKSPACE") = C:\Users\finchina\Desktop\Jenkins\finchina_Plugins\Plugins\work\jobs\demo\workspace
*
* String getPreviousBuildUrl = build.getPreviousBuild().getUrl();
* listener.getLogger().println("getPreviousBuildUrl = "+getPreviousBuildUrl);
* listener.getLogger().println("build.getUrl = "+build.getUrl());
* listener.getLogger().println("build.getId = "+build.getId());
* listener.getLogger().println("build.getDisplayName = "+build.getDisplayName());
* listener.getLogger().println("build.getEnvironment(listener).expand(\"testEnv\") = "+build.getEnvironment(listener).expand("testEnv"));
* listener.getLogger().println("build.getEnvironment(listener).get(\"testEnv\") = "+build.getEnvironment(listener).get("testEnv"));
* listener.getLogger().println("build.getEnvironment(listener).get(\"WORKSPACE\") = "+build.getEnvironment(listener).get("WORKSPACE"));
* */
/**
* 执行业务操作
*
* */
String workSpace = build.getEnvironment(listener).get("WORKSPACE");
List<String> list = Excution.gitDiff(new File(workSpace),consult_Branch,tag_Branch);
String[] strings = atUsers.split(",");
List<String> listatUsers = new ArrayList(Arrays.asList(strings)) ;
/**
* 组装消息通知内容
* */
String title = "**请查看项目:"+build.getEnvironment(listener).get("JOB_NAME")+"的代码diff报告**";
String Summary = "***Summary:"+list.get(list.size()-1)+"***";
StringBuffer StringBuffer = new StringBuffer();
for (int i = 0; i < list.size()-1; i++) {
StringBuffer.append("\n").append("> ").append(list.get(i));
}
/**
* 企业微信消息通知
* */
WeChartMess.actions(weChartUrl,listatUsers,title,Summary,"****Details:**** ",StringBuffer.toString());
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
public DescriptorImpl() {
load();
}
public FormValidation doCheckName(@QueryParameter String consult_Branch , @QueryParameter String tag_Branch)
throws IOException, ServletException {
if (consult_Branch.length() == 0 && tag_Branch.length() == 0)
return FormValidation.error("Please set a oldBranch and newBranch");
if (consult_Branch.length() < 4 && tag_Branch.length() < 4)
return FormValidation.warning("Isn't the oldBranch and newBranch too short?");
return FormValidation.ok();
}
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return true;
}
/**
* 插件的名称
*/
public String getDisplayName() {
return "源码Diff工具_Git";
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
save();
return super.configure(req,formData);
}
}
}
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* 执行命令
* */
public class Excution {
/**
* diff操作后获取执行命令的结果
* @param workspace 代码工作空间
* @param oldBranch diff的比较/参照分支
* @param fleashBranch 需要diff的分支
* @return List<String> 执行结果的List集合
* */
public static List<String> gitDiff(File workspace,String oldBranch,String fleashBranch){
/**
* 1:进入到workspace
* 2:执行git命令获取数据
* 3:封装数据为list集合
* */
String cmd = "git diff "+oldBranch+" "+fleashBranch+" --stat";
return exeCmd(cmd,workspace);
}
/**
* 执行linux命令 获取返回值组装成集合
* */
public static List<String> exeCmd(String commandStr, File workspace) {
List<String> list = new ArrayList();
try {
Process ps = Runtime.getRuntime().exec(commandStr,null,workspace);
BufferedReader br = new BufferedReader(new InputStreamReader( ps.getInputStream(), Charset.forName("UTF-8")));
String line;
while ((line = br.readLine()) != null) {
list.add(line);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import okhttp3.*;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 固定消息通知格式为markdown格式 其他的可以参考企业微信开发文档
* */
public class WeChartMess {
private static Object type = "markdown";
public static String sendWeChartNotices (String reqBody,String url) throws IOException {
OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS)// 设置连接超时时间
.readTimeout(20, TimeUnit.SECONDS)// 设置读取超时时间
.build();
MediaType contentType = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(contentType, reqBody);
Request request = new Request.Builder().url(url).post(body).addHeader("cache-control", "no-cache").build();
Response response = client.newCall(request).execute();
byte[] datas = response.body().bytes();
String respMsg = new String(datas);
return respMsg;
}
/**
* Map -> json
*
* 建议使用LinkedHashMap,因为LinkedHashMap有顺序
* */
public static String mapToJson(Map<String , Object> map){
return JSON.toJSONString(map);
}
/**
* List->String
* */
public static String listToString(List<String> list){
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < list.size(); i++) {
if(i != (list.size() -1)){
stringBuffer.append(list.get(i)+"\n");
}else{
stringBuffer.append(list.get(i));
}
}
return stringBuffer.toString();
}
/**
* 执行发布消息的操作
* */
public static String actions(String url,List<String> atUsers,String... content) throws IOException {
Map<String , Object> map = new LinkedHashMap();
Map<String , Object> text = new LinkedHashMap();
//list转String
List<String> list = Arrays.asList(content);
String str = listToString(list);
map.put("msgtype", type);
//拼接@xx的操作
StringBuffer buffer = new StringBuffer();
//@用户的操作,一般情况下企业微信的UserId就是公司邮箱的前缀
for(String atUserId : atUsers){
if(StrUtil.isNotEmpty(atUserId)){
buffer.append("<@");
buffer.append(atUserId);
buffer.append(">");
}
text.put("content",str+"\n"+buffer);
}
map.put(type.toString(),text);
String resBody = sendWeChartNotices(mapToJson(map),url);
return resBody;
}
}
//textbox:表示是一个文本输入框
<f:entry title="Consult_Branch" field="consult_Branch" description="上一个master分支或者发布分支">
<f:textbox />
</f:entry>
<f:entry title="Tag_Branch" field="tag_Branch" description="当前发布的Release分支或者发布分支">
<f:password />
</f:entry>
<f:advanced>
<f:entry title="BlockFiles" field="blockFiles" description="高级功能:黑名单(加入黑名单的文件将不会被diff 格式:*diff*、*.xml...)">
<f:textbox />
</f:entry>
<f:entry title="TagPaths" field="tagPaths" description="高级功能:目标paths">
<f:textbox />
</f:entry>
</f:advanced>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="Consult_Branch" field="consult_Branch" description="上一个master分支或者发布分支">
<f:textbox />
</f:entry>
<f:entry title="Tag_Branch" field="tag_Branch" description="当前发布的Release分支或者发布分支">
<f:textbox />
</f:entry>
<f:entry title="WeChartUrl" field="weChartUrl" description="企业微信消息通知url链接(包含token)">
<f:textbox />
</f:entry>
<f:entry title="@Users" field="atUsers" description="企业微信群@操作">
<f:textbox />
</f:entry>
<!-- 高级功能部分 -->
<f:advanced>
<f:entry title="BlockFiles" field="blockFiles" description="高级功能:黑名单(加入黑名单的文件将不会被diff 格式:*diff*、*.xml...)">
<f:textbox />
</f:entry>
<f:entry title="TagPaths" field="tagPaths" description="高级功能:目标paths">
<f:textbox />
</f:entry>
</f:advanced>
</j:jelly>
<div>
atUser:使用者可以输入企业微信用户ID,在当前构建步骤结束后会通知到对应的企业微信群并@相关人员
示例:zhangshang,lishi,wangwu
</div>
* 如下:是插件的 style 展示
* 企业微信通知效果如下