持续交付 [Jenkins 插件开发] Jenkins 插件二次开发 - 设计一个代码 diff 的小工具

yangbin · 2020年07月23日 · 最后由 yangbin 回复于 2020年07月27日 · 3998 次阅读

简介

1:为什么要开发这个工具

简要说一下:开发这个Jenkins插件的初衷是解决公司在代码管理上遇到的问题;现状是:我目前所在的这家公司技术上真的是老古董的那种,代码管理水平真的很一般(各种夹带...),所以不得已才需要做这个插件协助开发区diff文件甚至到单行代码的变更情况(目前由于技术水平有限,只diff出文件的变化;我觉得如果diff到具体谋行的变更最难的地方是数据要怎么展示、这个是一个难点... 数据的获取可以通过git命令抓取)

2:本文涉及到的 git diff 的简介

2.1:git 代码 diff 的原理
2.1.1:diff 分支之间的文件的变更
//如下:为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 要设置(如果是在拉取的代码的工作目录下这一步可以省略)

2.1.2:diff 分支代码行之间的变更
//如下:为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 分支增加的代码行

开始完成这 diff 工具

1:Jenkins 插件开发的准备工作 - 开始一个 Hello-Word

配置 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 功能

2:Jenkins 插件开发 - 代码 diff 插件

2.1:创建插件的类方法 需要继承 Builder 和实现 SimpleBuildStep 接口
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);
        }
    }
}
2.2:执行 diff git 命令的类方法的封装
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;
    }
}
2.3:企业微信消息通知 (代码 diff 结束将会发送消息给对应的用户)
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;
    }
}
2.4:完成 jelly 文件的配置,进行界面的参数化
  • 对于开发这作用于构建过程中的插件只有修改 config.jelly 文件即可 ###### 2.4.1:jelly 文件的简单介绍
  • jelly 文件类似于 html 文件,但是他跟 html 又有很大的不同,在 Jenkins 插件开发过程中可以使用 Groovy 代替 jelly(这里我还在研究中,有懂的可以分享出来,目前 Jenkins 插件开发的中文资料还是比较少的)
  • texbox
//textbox:表示是一个文本输入框
<f:entry title="Consult_Branch" field="consult_Branch" description="上一个master分支或者发布分支">
    <f:textbox />
  </f:entry>
  • password:表示是一个密码框,内容会以密文展示
<f:entry title="Tag_Branch" field="tag_Branch" description="当前发布的Release分支或者发布分支">
    <f:password />
</f:entry>
  • advanced:标记的会展示在"高级"小,默认折叠,点击"高级"会展开
<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>
2.4.2:config.jelly 文件
<?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>
2.4.3:help.html 文件
  • 命名规则是 help-field.html
<div>
  atUser:使用者可以输入企业微信用户ID,在当前构建步骤结束后会通知到对应的企业微信群并@相关人员
  示例:zhangshang,lishi,wangwu
</div>
2.4.4:mvn hpi:run 本地查看插件开发情况

* 如下:是插件的 style 展示

* 企业微信通知效果如下

共收到 7 条回复 时间 点赞

GitLab 是不是自带 diff 功能?

水寒 回复

看来你没细看啊😂 ,是把 git 的 diff 功能集成在 jenkins 上,有两方面的目的
其一:如何开发一个 jenkins 插件
第二:将手动 diff 变为自动 diff

水寒 回复

git 的手动 diff 以及 vimdiff 都可以的

我脚的这个应该是你想实现的?
https://plugins.jenkins.io/last-changes/

simple 回复

好的 我拜读一下 感谢你的分享

挺好的。
包含一个想法的提出和实现。

Ouroboros 回复

感谢鼓励,首次发帖,争取之后能给大家提供一些更有价值的东西,一起为行业做出自己的贡献😀

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册