vivo 互聯(lián)網(wǎng)自研代碼評審 VCR 落地實(shí)踐
代碼評審是軟件質(zhì)量保證一種活動(dòng),由一個(gè)或者多個(gè)人對一個(gè)程序的部分或者全部源代碼進(jìn)閱讀理解。一般來說分為作者和評審者兩種角色,作者方提供代碼邏輯的介紹和代碼,評審者則對提供的代碼基于設(shè)計(jì),功能性和非功能性等方面認(rèn)知進(jìn)行閱讀并提出問題。常見的評審組織形式是有同行評審(Peer Review)和小組檢查 (Team Inspection)兩種方式。
在代碼評審中,評審的目的在通過代碼的評審發(fā)現(xiàn)潛在的問題,同時(shí)分享和表達(dá)是代碼評審的重要收獲,我們知道人相同在不同的文化下生產(chǎn)力是不同的,代碼評審是一個(gè)工具,工具受文化的影響的同時(shí)也影響著文化,最終朝著我們希望的責(zé)任共擔(dān)、持續(xù)改進(jìn)的方向發(fā)展。
一、代碼評審演進(jìn)
隨著互聯(lián)網(wǎng)的發(fā)展,開發(fā)人員也越來越重視代碼評審帶來的代碼的代碼質(zhì)量提高以及代碼評審間接帶來的分享及人員備份效果,已經(jīng)不滿足于只是簡單的發(fā)現(xiàn)當(dāng)前問題解決問題記錄問題,需要滿足從評審基本跟進(jìn)、評論管理、評審報(bào)告以及評審方式多樣化、評審與研發(fā)流程相結(jié)合等需求。
① 代碼評審檢查表:手工定義要檢查項(xiàng),檢查完進(jìn)行打卡標(biāo)記結(jié)果。
② 插件快速評審導(dǎo)入導(dǎo)出:快速在插件上進(jìn)行評論,并將評論結(jié)果導(dǎo)出給被評審人,被評審人導(dǎo)入評審結(jié)果查看,評審表不可復(fù)用,一旦代碼變更則無法準(zhǔn)確定位、也無法再次跟蹤評審修改結(jié)果。
③ 在線代碼評審:在線插件或網(wǎng)頁評審,提供提交前提交后評審,可多人評審策略管控、代碼評審與需求/缺陷關(guān)聯(lián)管理。
④ 自動(dòng)化代碼評審:結(jié)合現(xiàn)有的Sonar掃描、安全掃描進(jìn)行對提交的代碼進(jìn)行自動(dòng)化檢查,使代碼在人工評審之前已經(jīng)經(jīng)歷一輪自動(dòng)評審,代碼評審?fù)ㄟ^之后可自動(dòng)觸發(fā)構(gòu)建、部署等。
⑤ 智能化代碼評審:根據(jù)AI大模型,可對提交代碼進(jìn)行綜合評價(jià)(編碼標(biāo)準(zhǔn)、可用性、可讀性、可維護(hù)性、安全性、高性能、異??刂啤⒃O(shè)計(jì)原則、可擴(kuò)展性、代碼復(fù)雜度等等)并給出相關(guān)測試建議等,未來大模型對代碼評審還有更大的空間。
二、代碼評審解決的需求和痛點(diǎn)是什么?
vivo當(dāng)前已經(jīng)有EasyCR評審工具,那為什么我們還需要繼續(xù)開發(fā)調(diào)研代碼評審工具呢?
我們先看看下面通過內(nèi)部調(diào)研獲取的信息,看看用戶希望的代碼評審工具需求和痛點(diǎn)是什么?
針對當(dāng)前vivo代碼評審工具我們繼續(xù)升級補(bǔ)充場景:
- 增加評審方式:對原自由評審方式(主要是提交后進(jìn)行代碼評審)增加評審控制方式(提交代碼至倉庫前進(jìn)行代碼評審、合并時(shí)提交代碼評審)。
- 支持網(wǎng)頁/插件:增加網(wǎng)頁端評審功能,滿足不同角色進(jìn)行評審及用戶體驗(yàn)上的優(yōu)化,增強(qiáng)插件版評審功能。
- 支持研發(fā)流程控制:上線過程中可作為人工卡點(diǎn)一項(xiàng)檢查項(xiàng)(可通過代碼是否評審、代碼評分、代碼問題解決情況等進(jìn)行判斷),通過線上管理,提高上線質(zhì)量。
- 支持自動(dòng)化檢查:代碼提交前,提交后可進(jìn)行代碼自動(dòng)化檢查,對代碼進(jìn)行自動(dòng)評審。
- 增加用戶定制化需求:如評審權(quán)限、評審?fù)ㄖ绞?、評審策略多人評審管理、評審報(bào)告訂閱等。
當(dāng)前市場上有很多優(yōu)秀的代碼評審工具,但是很少有評審工具能滿足所有的場景,角色不同,需要的能力不同,同一個(gè)角色不同團(tuán)隊(duì)使用的方式不同,我們需要一款解決用戶痛癢爽的代碼評審工具。
三、vivo代碼評審系統(tǒng)架構(gòu)
四、vivo代碼評審工具使用流程
在代碼評審中,CR可以是一次Commit,也可以是一次MergeCommit,那么針對一次CR我們可以隨時(shí)對已經(jīng)提交的commit進(jìn)行評審,也可以在CRpush至代碼庫之前攔截,同時(shí)也可以在一次合并之前進(jìn)行代碼評審。
代碼評審模式:
1. 提交前評審(Pre-push Code Review)
2. 提交后評審(Post-push Code Review)
① 合并評審
② 自由評審
提交前評審:VCR基于VCR在提交push至Gitlab代碼倉庫之前,對代碼進(jìn)行攔截,并進(jìn)行評審,支持一次評審請求作為一次評審,可對一次一次評審請求查看所有變更記錄并進(jìn)行評審追蹤。利用開源工具Gerrit,將評審請求推送至Gerrit中,評審?fù)ㄟ^后,將代碼從Gerrit同步至Gitlab倉庫
提交后評審:
①合并評審:VCR基于Gitlab 在一次MR的基礎(chǔ)上進(jìn)行代碼評審。
②自由評審:針對用戶當(dāng)前代碼庫當(dāng)前分支信息或歷史commit進(jìn)行評審。
五、vivo代碼評審工具實(shí)施
5.1 確認(rèn)技術(shù)架構(gòu)
提交倉庫前進(jìn)行代碼評審,我們使用當(dāng)前成熟的代碼評審Gerrit,實(shí)施過程中最大的問題是用戶如何低成本切換及簡單評審的問題,對于當(dāng)前Gerrit評審工具遇到的問題如何解決呢?
1) 我們知道Gerrit評審工具需要提供給用戶Gerrit代碼庫地址,并進(jìn)行下載使用,當(dāng)前用戶使用的代碼庫習(xí)慣不能更改,也是不愿意修改的,那么我們?nèi)绾谓鉀Q呢?
給插件加持,提供用戶黑盒切換至評審代碼庫,或執(zhí)行一鍵下載代碼庫功能,底層使用Gerrit與代碼托管庫同步機(jī)制解決代碼一致性問題,用戶在使用代碼庫時(shí)同原使用方式一致。
2) Git代碼提交,CR為最小單位,CR可作為一次評審,但還有很多用戶使用的習(xí)慣是一次push作為一次評審,如何解決用戶一次push為一次評審呢?
a)需要對代碼關(guān)系鏈需要進(jìn)行整理,識別出一次push作為一次評審記錄,用戶多次追加提交記錄至評審請求,需要重新識別出關(guān)系鏈作為原push請求的評審記錄,Git原生對代碼變更的情況比較多,我們對一些場景進(jìn)行分析再特殊處理,不窮舉。
b)可對最小粒度CR的評審,也同時(shí)提供一次push請求內(nèi)容進(jìn)行評審,更方便快捷。
用戶不管是提交前評審、合并時(shí)評審,都可能會產(chǎn)生一次push,多次commit,用戶需要對最小粒度CR評審,也需要對最新變更所有內(nèi)容進(jìn)行評審。
5.2 插件改造實(shí)施
根據(jù)我們對用戶的調(diào)研過程中,用戶對代碼評審插件網(wǎng)頁同時(shí)兼容的要求比較高,針對idea插件我們?nèi)绾胃脑齑a評審,這里我們著重對Gerrit插件改造展開說明。
步驟1:了解插件框架、配置、打包、運(yùn)行
1)插件框架整體介紹
(圖片來源于網(wǎng)絡(luò))
- 開發(fā)方式:在官網(wǎng)的描述中,創(chuàng)建IDEA插件工程的方式有兩種分別是使用DevKit(IntelliJ Platform Plugin 模版創(chuàng)建)和Gradle構(gòu)建方式,這兩種方式在構(gòu)建項(xiàng)目和打包發(fā)布上有所區(qū)別,同時(shí)官方提供了將Devkit遷移至Gradle的方式。
參考:https://plugins.jetbrains.com/docs/intellij/developing-plugins.html - 框架入口:一個(gè) IDEA 插件開發(fā)完,要考慮把它嵌入到哪,比如是從 IDEA 窗體的 Edit、Tools 等進(jìn)入配置還是把窗體嵌入到左、右工具條還是IDEA窗體下的對話框。
- UI:思考的是窗體需要用到什么語言開發(fā),沒錯(cuò),用的就是 Swing、Awt 的技術(shù)能力。
- API:在 IDEA 插件開發(fā)中,一般都是圍繞工程進(jìn)行的,那么基本要從通過 IDEA 插件 JDK 開發(fā)能力中獲取到工程信息、類信息、文件信息等。
- 外部功能:這一個(gè)是用于把插件能力與外部系統(tǒng)結(jié)合,比如你是需要把拿到的接口上傳到服務(wù)器,還是從遠(yuǎn)程下載文件等等。
2)Gradle創(chuàng)建
新版通過 New-> Project->IDE Plugin進(jìn)行創(chuàng)建,舊版通過New Project->Gradle->IntelliJ Platform Plugin進(jìn)行創(chuàng)建。
項(xiàng)目結(jié)構(gòu)如下:
3)配置介紹
plugin.xml
<!DOCTYPE idea-plugin PUBLIC "Plugin/DTD" "http://xxxx">
<idea-plugin>
<!-- 插件唯一id,不能和其他插件項(xiàng)目重復(fù),所以推薦使用包名+插件名com.xxx.xxx的格式
插件不同版本之間不能更改,若沒有指定,則與name相同 -->
<id> com.your.company.unique.plugin.id </id>
<!-- 插件名稱,別人在官方插件庫搜索你的插件時(shí)使用的名稱 -->
<name> Plugin display name here </name>
<!-- 插件版本,格式:BRANCH.BUILD.FIX (MAJOR.MINOR.FIX) -->vs
<version>1.0.0</version>
<!-- 供應(yīng)商主頁和email(不能使用默認(rèn)值,必須修改成自己的)-->
<vendor email="support@yourcompany.com" url="https://www.yourcompany.com">YourCompany</vendor>
<!-- 插件的描述 (不能使用默認(rèn)值,必須修改成自己的。并且需要大于40個(gè)字符)-->
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>
<!-- 插件版本變更信息,使用<![CDATA[ ]]> 來支持HTML格式;
將展示在 settings | Plugins 對話框和插件倉庫的Web頁面 -->
<change-notes><![CDATA[
<p>
<li>1.0.0</li>
<ul>
<li>
1.新增xxx功能 <br/>
2.優(yōu)化xxx功能 <br/>
</li>
</ul>
</p>
]]>
</change-notes>
<!-- please see http://confluence.jetbrains.net/display/IDEADEV/Build+Number+Ranges for description -->
<!-- 插件兼容構(gòu)建的IDE版本, until-build可以不寫,默認(rèn)到最新版 -->
<idea-version since-build="203.4818.26" until-build="211"/>
<!-- please see http://confluence.jetbrains.net/display/IDEADEV/Plugin+Compatibility+with+IntelliJ+Platform+Products
on how to target different products -->
<!-- 插件依賴,可以依賴模塊或插件 -->
<depends>com.intellij.modules.lang</depends>
<depends>Git4Idea</depends>
<depends optional="true" config-file="plugin-maven.xml">org.jetbrains.idea.maven</depends>
<!—idea第一次打開, 實(shí)際上就是訂閱了應(yīng)用程序打開的事件-->
<application-components>
<component>
<implementation-class>xxxxx</implementation-class>
</component>
</application-components>
<!—打開項(xiàng)目 -->
<project-components>
<component>
<implementation-class>
xxxxx
</implementation-class>
</component>
</project-components>
<!-- 插件定義的擴(kuò)展點(diǎn),以供其他插件擴(kuò)展該插件,類似Java的抽象類的功能
如何在https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html -->
<extensionPoints>
</extensionPoints>
<!-- 聲明該插件對IDEA core或其他插件的擴(kuò)展,Ns是NameSpace的縮寫 -->
<extensions defaultExtensionNs="com.intellij">
<toolWindow id="代碼評審" icon="/icons/xx_13x13.png" anchor="bottom" factoryClass="xxx" />
</extensions>
<!-- 編寫插件動(dòng)作 https://plugins.jetbrains.com/docs/intellij/plugin-actions.html-->
<actions>
<action id="com.xx.xx.AddCommentAction"
class="com.xx.xx.actions.AddCommentAction"
text="添加評論"
description="為選中的代碼添加評論意見"
icon="AllIcons.Actions.StartDebugger">
<!—編輯器右鍵彈出菜單--!>
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
<!--快捷方式--!>
<keyboard-shortcut first-keystroke="alt X" keymap="$default"/> </action>
</action>
</actions>
</idea-plugin>
4)插件運(yùn)行調(diào)試打包安裝
Gradle構(gòu)建方式進(jìn)行調(diào)試打包安裝
運(yùn)行/調(diào)試:runIde 可以選擇Debug模式或者是Run模式
打包
安裝:可以將打的包發(fā)布市場(本地idea配置插件倉庫),從Marketplace搜索插件或者是直接從Settings->plugins->Install->Install Plugin from Disk安裝
步驟2:研究Gerrit插件源碼,搞清楚整理開發(fā)流程和模塊
步驟3:基于Gerrit插件規(guī)劃VCR插件模塊,增加clone、branch、mergeRequest、VCR模塊,并對各組件增強(qiáng)
步驟4:定制原有流程模塊push,自動(dòng)化關(guān)聯(lián)工作項(xiàng)
在使用Git依賴插件之前,先了解一下插件的擴(kuò)展以及擴(kuò)展點(diǎn)(Extensions、Extension Points)。
Intellij 平臺提供了允許一個(gè)插件與其他插件或者 IDE 交互的 extensions 以及 extension points 的概念。
- Extension Points:如果你想要你的插件可以被其他插件使用,那么你必須在你的插件內(nèi)聲明一個(gè)或多個(gè)擴(kuò)展點(diǎn)(extension points)。每個(gè)擴(kuò)展點(diǎn)定義了允許訪問這個(gè)點(diǎn)的類或者接口。
- Extensions:如果你想要你的插件擴(kuò)展其他插件或者 Intellij 平臺,你必須聲明一個(gè)或多個(gè) extensions。
可以在 plugin.xml 中的和塊中定義 extensions 以及 extension points。
plugin.xml
<!--依賴插件包--!>
<depends>Git4Idea</depends>
<!—idea第一次打開, 實(shí)際上就是訂閱了應(yīng)用程序打開的事件-->
<application-components>
<component>
<implementation-class>com.demo.intellij.plugin.vcr.push.VcrPushExtension$Proxy</implementation-class>
</component>
</application-components>
上述我們看到依賴的Git4Idea 包,如果我們想修改原生的的Git,先看下push依賴包中如何實(shí)現(xiàn)的。
Git4Idea(plugin.xml)
<extensions defaultExtensionNs="com.intellij">
<pushSupport implementation="git4idea.push.GitPushSupport"/>
...
</extensions>
intellij-dvcs.jar(plugin.xml)
<extensionPoints>
<extensionPoint name="pushSupport"
interface="com.intellij.dvcs.push.PushSupport"
area="IDEA_PROJECT"
dynamic="true"/>
....
</extensionPoints>
從上述可看到,Git4Idea 的GitPushSupport擴(kuò)展實(shí)現(xiàn)push的功能點(diǎn),接下來我們主要對GitPushSupport進(jìn)行javassist字節(jié)碼修改以達(dá)到擴(kuò)展git push組件能力。
擴(kuò)展使用GitPushSupport之前,需要將需要的類進(jìn)行裝載至GitPlugin中,然后再對GitPushSupport進(jìn)行字節(jié)碼改造,至此對git Push原生插件頁進(jìn)行改造。
步驟5:使用樹狀列表模式,展示一次push請求VCR提交內(nèi)容及多個(gè)CR情況
主要是實(shí)現(xiàn)JTreeTable,對VCR與CR進(jìn)行管理。
一次評審請求VCR包含所有CR的提交變更記錄,可針對該變更記錄進(jìn)行代碼評審,單個(gè)CR也可以進(jìn)行評審。
步驟6:展示變更文件視圖及定制評論展示模塊,精準(zhǔn)定位代碼
代碼評審主要根據(jù)編輯器獲取代碼行及位置,評論可精準(zhǔn)定位到代碼行。
1)changeBrowser變更視圖展示VCR變更文件信息
2)雙擊文件,diff視圖展示inline和side-by-side兩種代碼差異
聲明擴(kuò)展,針對擴(kuò)展類進(jìn)行定制化改造。
plugin.xml
<diff.DiffTool implementatinotallow="com.demo.intellij.plugin.vcr.ui.diff.VcrCommentsDiffTool$Proxy"/>
3)添加代碼塊評論,定位代碼塊
AddCommentAction.java
public class AddCommentAction extends AnAction implements DumbAware {
public AddCommentAction(String label,
Icon icon,
CommentsDiffTool commentsDiffTool,
Editor editor,
List<CommentInfo> fileComments
....
) {
super(label, null, icon);
}
private CommentInput createComment() {
//獲取用戶選擇代碼位置位置
//行的情況下,默認(rèn)是開頭和行結(jié)束 得到光標(biāo)的位置caretModel.getOffset();
/*取到插字光標(biāo)模式對象 CaretModel caretModel = editor.getCaretModel();
得到光標(biāo)的位置int caretOffset = caretModel.getOffset();
//得到一行開始和結(jié)束的地方
int lineNum = document.getLineNumber(caretOffset);
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
獲取一行內(nèi)容String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
*/
Document document = editor.getDocument();
int lineNum = document.getLineNumber(editor.getCaretModel().getOffset()) ;
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
String lineContent = document.getText(new TextRange(lineStartOffset, lineEndOffset));
.....
}
}
所有評論展示列表如何精準(zhǔn)定位代碼
SafeHtmlHistoryComments.java
public class SafeHtmlHistoryComments extends JPanel {
private Iterable<CommentInfo> fileComments;
private List<CommentInfo> commentInfos = new ArrayList<>();
private CommentInfo currentCommentInfo;
private SelectedComment selectedComment;
private SelectedComment operatorSelectedComment;
private Editor editor;
public SafeHtmlHistoryComments(Editor editor,Iterable<CommentInfo> fileComments, Comment selectedComment) {
super(new BorderLayout());
....
HistoryCommentListPanel historyCommentListPanel = new HistoryCommentListPanel(fileComments);
//雙擊table某行觸發(fā)代碼定位
historyCommentListPanel.addTableMouseDoubleHit(new Consumer<CommentInfo>() {
@Override
public void consume(CommentInfo commentInfo) {
codeTextHit(editor,commentInfo);
}
});
}
/**
* 定位代碼
* @param editor
* @param commentInfo
*/
private static void codeTextHit(Editor editor, CommentInfo commentInfo) {
SelectionModel selectionModel = editor.getSelectionModel();
// 優(yōu)化:如果文件修改過了,則不進(jìn)行選中操作,換為提示
if (null != commentInfo.startIndex && null != commentInfo.endIndex && commentInfo.startIndex != 0 && commentInfo.endIndex != 0) {
editor.getCaretModel().moveToOffset(commentInfo.endIndex);
selectionModel.setSelection(commentInfo.startIndex, commentInfo.endIndex);
} else if (null != commentInfo.line && commentInfo.line != 0) {
int lineNum = commentInfo.line - 1;
editor.getCaretModel().moveToOffset(lineNum);
CharSequence charsSequence = editor.getMarkupModel().getDocument().getCharsSequence();
if(null!=commentInfo.range) {
RangeUtils.Offset offset = RangeUtils.rangeToTextOffset(charsSequence, commentInfo.range);
selectionModel.setSelection(offset.start, offset.end);
}else{
Document document = editor.getDocument();
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
selectionModel.setSelection(lineStartOffset, lineEndOffset);
}
}
editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
....
}
六、未來展望
6.1 自動(dòng)化代碼評審
- 代碼提交評審或代碼合并之前,先自動(dòng)化檢查(Sonar/安全掃描)快速發(fā)現(xiàn)并糾正潛在問題,檢查成功后提交評審。
- 代碼評審?fù)ㄟ^之后,結(jié)合流水線,自定義部署構(gòu)建策略,實(shí)現(xiàn)快速迭代。
- 自動(dòng)匯聚測試報(bào)告,根據(jù)評審問題類型進(jìn)行分類,不斷改進(jìn)Sonar檢查規(guī)則,從而形成良性循環(huán)。
6.2智能化代碼評審
- 提交代碼評審之后,通過AI大模型對代碼進(jìn)行綜合評價(jià),并給出建議。
- 通過智能代碼評審,產(chǎn)生評審報(bào)告,并進(jìn)行智能化分析。