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

yangbin發表於2020-07-23

簡介

1:為什麼要開發這個工具

簡要說一下:開發這個Jenkins外掛的初衷是解決公司在程式碼管理上遇到的問題;現狀是:我目前所在的這家公司技術上真的是老古董的那種(我是今年3月份被裁員後入職的),程式碼管理水平真的很一般(各種夾帶...),所以不得已才需要做這個外掛協助開發區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展示

*企業微信通知效果如下

相關文章