[Jenkins 外掛開發] Jenkins 外掛二次開發 - 設計一個程式碼 diff 的小工具

yangbin發表於2020-07-23

簡介

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 展示

* 企業微信通知效果如下

相關文章