vivo 互聯網自研代碼評審 VCR 落地實踐
代碼評審是軟件質量保證一種活動,由一個或者多個人對一個程序的部分或者全部源代碼進閱讀理解。一般來說分為作者和評審者兩種角色,作者方提供代碼邏輯的介紹和代碼,評審者則對提供的代碼基于設計,功能性和非功能性等方面認知進行閱讀并提出問題。常見的評審組織形式是有同行評審(Peer Review)和小組檢查 (Team Inspection)兩種方式。
在代碼評審中,評審的目的在通過代碼的評審發現潛在的問題,同時分享和表達是代碼評審的重要收獲,我們知道人相同在不同的文化下生產力是不同的,代碼評審是一個工具,工具受文化的影響的同時也影響著文化,最終朝著我們希望的責任共擔、持續改進的方向發展。
一、代碼評審演進
隨著互聯網的發展,開發人員也越來越重視代碼評審帶來的代碼的代碼質量提高以及代碼評審間接帶來的分享及人員備份效果,已經不滿足于只是簡單的發現當前問題解決問題記錄問題,需要滿足從評審基本跟進、評論管理、評審報告以及評審方式多樣化、評審與研發流程相結合等需求。
① 代碼評審檢查表:手工定義要檢查項,檢查完進行打卡標記結果。
② 插件快速評審導入導出:快速在插件上進行評論,并將評論結果導出給被評審人,被評審人導入評審結果查看,評審表不可復用,一旦代碼變更則無法準確定位、也無法再次跟蹤評審修改結果。
③ 在線代碼評審:在線插件或網頁評審,提供提交前提交后評審,可多人評審策略管控、代碼評審與需求/缺陷關聯管理。
④ 自動化代碼評審:結合現有的Sonar掃描、安全掃描進行對提交的代碼進行自動化檢查,使代碼在人工評審之前已經經歷一輪自動評審,代碼評審通過之后可自動觸發構建、部署等。
⑤ 智能化代碼評審:根據AI大模型,可對提交代碼進行綜合評價(編碼標準、可用性、可讀性、可維護性、安全性、高性能、異常控制、設計原則、可擴展性、代碼復雜度等等)并給出相關測試建議等,未來大模型對代碼評審還有更大的空間。
二、代碼評審解決的需求和痛點是什么?
vivo當前已經有EasyCR評審工具,那為什么我們還需要繼續開發調研代碼評審工具呢?
我們先看看下面通過內部調研獲取的信息,看看用戶希望的代碼評審工具需求和痛點是什么?
針對當前vivo代碼評審工具我們繼續升級補充場景:
- 增加評審方式:對原自由評審方式(主要是提交后進行代碼評審)增加評審控制方式(提交代碼至倉庫前進行代碼評審、合并時提交代碼評審)。
- 支持網頁/插件:增加網頁端評審功能,滿足不同角色進行評審及用戶體驗上的優化,增強插件版評審功能。
- 支持研發流程控制:上線過程中可作為人工卡點一項檢查項(可通過代碼是否評審、代碼評分、代碼問題解決情況等進行判斷),通過線上管理,提高上線質量。
- 支持自動化檢查:代碼提交前,提交后可進行代碼自動化檢查,對代碼進行自動評審。
- 增加用戶定制化需求:如評審權限、評審通知方式、評審策略多人評審管理、評審報告訂閱等。
當前市場上有很多優秀的代碼評審工具,但是很少有評審工具能滿足所有的場景,角色不同,需要的能力不同,同一個角色不同團隊使用的方式不同,我們需要一款解決用戶痛癢爽的代碼評審工具。
三、vivo代碼評審系統架構
四、vivo代碼評審工具使用流程
在代碼評審中,CR可以是一次Commit,也可以是一次MergeCommit,那么針對一次CR我們可以隨時對已經提交的commit進行評審,也可以在CRpush至代碼庫之前攔截,同時也可以在一次合并之前進行代碼評審。
代碼評審模式:
1. 提交前評審(Pre-push Code Review)
2. 提交后評審(Post-push Code Review)
① 合并評審
② 自由評審
提交前評審:VCR基于VCR在提交push至Gitlab代碼倉庫之前,對代碼進行攔截,并進行評審,支持一次評審請求作為一次評審,可對一次一次評審請求查看所有變更記錄并進行評審追蹤。利用開源工具Gerrit,將評審請求推送至Gerrit中,評審通過后,將代碼從Gerrit同步至Gitlab倉庫
提交后評審:
①合并評審:VCR基于Gitlab 在一次MR的基礎上進行代碼評審。
②自由評審:針對用戶當前代碼庫當前分支信息或歷史commit進行評審。
五、vivo代碼評審工具實施
5.1 確認技術架構
提交倉庫前進行代碼評審,我們使用當前成熟的代碼評審Gerrit,實施過程中最大的問題是用戶如何低成本切換及簡單評審的問題,對于當前Gerrit評審工具遇到的問題如何解決呢?
1) 我們知道Gerrit評審工具需要提供給用戶Gerrit代碼庫地址,并進行下載使用,當前用戶使用的代碼庫習慣不能更改,也是不愿意修改的,那么我們如何解決呢?
給插件加持,提供用戶黑盒切換至評審代碼庫,或執行一鍵下載代碼庫功能,底層使用Gerrit與代碼托管庫同步機制解決代碼一致性問題,用戶在使用代碼庫時同原使用方式一致。
2) Git代碼提交,CR為最小單位,CR可作為一次評審,但還有很多用戶使用的習慣是一次push作為一次評審,如何解決用戶一次push為一次評審呢?
a)需要對代碼關系鏈需要進行整理,識別出一次push作為一次評審記錄,用戶多次追加提交記錄至評審請求,需要重新識別出關系鏈作為原push請求的評審記錄,Git原生對代碼變更的情況比較多,我們對一些場景進行分析再特殊處理,不窮舉。
b)可對最小粒度CR的評審,也同時提供一次push請求內容進行評審,更方便快捷。
用戶不管是提交前評審、合并時評審,都可能會產生一次push,多次commit,用戶需要對最小粒度CR評審,也需要對最新變更所有內容進行評審。
5.2 插件改造實施
根據我們對用戶的調研過程中,用戶對代碼評審插件網頁同時兼容的要求比較高,針對idea插件我們如何改造代碼評審,這里我們著重對Gerrit插件改造展開說明。
步驟1:了解插件框架、配置、打包、運行
1)插件框架整體介紹
(圖片來源于網絡)
- 開發方式:在官網的描述中,創建IDEA插件工程的方式有兩種分別是使用DevKit(IntelliJ Platform Plugin 模版創建)和Gradle構建方式,這兩種方式在構建項目和打包發布上有所區別,同時官方提供了將Devkit遷移至Gradle的方式。
參考:https://plugins.jetbrains.com/docs/intellij/developing-plugins.html - 框架入口:一個 IDEA 插件開發完,要考慮把它嵌入到哪,比如是從 IDEA 窗體的 Edit、Tools 等進入配置還是把窗體嵌入到左、右工具條還是IDEA窗體下的對話框。
- UI:思考的是窗體需要用到什么語言開發,沒錯,用的就是 Swing、Awt 的技術能力。
- API:在 IDEA 插件開發中,一般都是圍繞工程進行的,那么基本要從通過 IDEA 插件 JDK 開發能力中獲取到工程信息、類信息、文件信息等。
- 外部功能:這一個是用于把插件能力與外部系統結合,比如你是需要把拿到的接口上傳到服務器,還是從遠程下載文件等等。
2)Gradle創建
新版通過 New-> Project->IDE Plugin進行創建,舊版通過New Project->Gradle->IntelliJ Platform Plugin進行創建。
項目結構如下:
3)配置介紹
plugin.xml
<!DOCTYPE idea-plugin PUBLIC "Plugin/DTD" "http://xxxx">
<idea-plugin>
<!-- 插件唯一id,不能和其他插件項目重復,所以推薦使用包名+插件名com.xxx.xxx的格式
插件不同版本之間不能更改,若沒有指定,則與name相同 -->
<id> com.your.company.unique.plugin.id </id>
<!-- 插件名稱,別人在官方插件庫搜索你的插件時使用的名稱 -->
<name> Plugin display name here </name>
<!-- 插件版本,格式:BRANCH.BUILD.FIX (MAJOR.MINOR.FIX) -->vs
<version>1.0.0</version>
<!-- 供應商主頁和email(不能使用默認值,必須修改成自己的)-->
<vendor email="support@yourcompany.com" url="https://www.yourcompany.com">YourCompany</vendor>
<!-- 插件的描述 (不能使用默認值,必須修改成自己的。并且需要大于40個字符)-->
<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.優化xxx功能 <br/>
</li>
</ul>
</p>
]]>
</change-notes>
<!-- please see http://confluence.jetbrains.net/display/IDEADEV/Build+Number+Ranges for description -->
<!-- 插件兼容構建的IDE版本, until-build可以不寫,默認到最新版 -->
<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第一次打開, 實際上就是訂閱了應用程序打開的事件-->
<application-components>
<component>
<implementation-class>xxxxx</implementation-class>
</component>
</application-components>
<!—打開項目 -->
<project-components>
<component>
<implementation-class>
xxxxx
</implementation-class>
</component>
</project-components>
<!-- 插件定義的擴展點,以供其他插件擴展該插件,類似Java的抽象類的功能
如何在https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html -->
<extensionPoints>
</extensionPoints>
<!-- 聲明該插件對IDEA core或其他插件的擴展,Ns是NameSpace的縮寫 -->
<extensions defaultExtensionNs="com.intellij">
<toolWindow id="代碼評審" icon="/icons/xx_13x13.png" anchor="bottom" factoryClass="xxx" />
</extensions>
<!-- 編寫插件動作 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)插件運行調試打包安裝
Gradle構建方式進行調試打包安裝
運行/調試:runIde 可以選擇Debug模式或者是Run模式
打包
安裝:可以將打的包發布市場(本地idea配置插件倉庫),從Marketplace搜索插件或者是直接從Settings->plugins->Install->Install Plugin from Disk安裝
步驟2:研究Gerrit插件源碼,搞清楚整理開發流程和模塊
步驟3:基于Gerrit插件規劃VCR插件模塊,增加clone、branch、mergeRequest、VCR模塊,并對各組件增強
步驟4:定制原有流程模塊push,自動化關聯工作項
在使用Git依賴插件之前,先了解一下插件的擴展以及擴展點(Extensions、Extension Points)。
Intellij 平臺提供了允許一個插件與其他插件或者 IDE 交互的 extensions 以及 extension points 的概念。
- Extension Points:如果你想要你的插件可以被其他插件使用,那么你必須在你的插件內聲明一個或多個擴展點(extension points)。每個擴展點定義了允許訪問這個點的類或者接口。
- Extensions:如果你想要你的插件擴展其他插件或者 Intellij 平臺,你必須聲明一個或多個 extensions。
可以在 plugin.xml 中的和塊中定義 extensions 以及 extension points。
plugin.xml
<!--依賴插件包--!>
<depends>Git4Idea</depends>
<!—idea第一次打開, 實際上就是訂閱了應用程序打開的事件-->
<application-components>
<component>
<implementation-class>com.demo.intellij.plugin.vcr.push.VcrPushExtension$Proxy</implementation-class>
</component>
</application-components>
上述我們看到依賴的Git4Idea 包,如果我們想修改原生的的Git,先看下push依賴包中如何實現的。
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擴展實現push的功能點,接下來我們主要對GitPushSupport進行javassist字節碼修改以達到擴展git push組件能力。
擴展使用GitPushSupport之前,需要將需要的類進行裝載至GitPlugin中,然后再對GitPushSupport進行字節碼改造,至此對git Push原生插件頁進行改造。
步驟5:使用樹狀列表模式,展示一次push請求VCR提交內容及多個CR情況
主要是實現JTreeTable,對VCR與CR進行管理。
一次評審請求VCR包含所有CR的提交變更記錄,可針對該變更記錄進行代碼評審,單個CR也可以進行評審。
步驟6:展示變更文件視圖及定制評論展示模塊,精準定位代碼
代碼評審主要根據編輯器獲取代碼行及位置,評論可精準定位到代碼行。
1)changeBrowser變更視圖展示VCR變更文件信息
2)雙擊文件,diff視圖展示inline和side-by-side兩種代碼差異
聲明擴展,針對擴展類進行定制化改造。
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() {
//獲取用戶選擇代碼位置位置
//行的情況下,默認是開頭和行結束 得到光標的位置caretModel.getOffset();
/*取到插字光標模式對象 CaretModel caretModel = editor.getCaretModel();
得到光標的位置int caretOffset = caretModel.getOffset();
//得到一行開始和結束的地方
int lineNum = document.getLineNumber(caretOffset);
int lineStartOffset = document.getLineStartOffset(lineNum);
int lineEndOffset = document.getLineEndOffset(lineNum);
獲取一行內容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));
.....
}
}
所有評論展示列表如何精準定位代碼
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某行觸發代碼定位
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();
// 優化:如果文件修改過了,則不進行選中操作,換為提示
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 自動化代碼評審
- 代碼提交評審或代碼合并之前,先自動化檢查(Sonar/安全掃描)快速發現并糾正潛在問題,檢查成功后提交評審。
- 代碼評審通過之后,結合流水線,自定義部署構建策略,實現快速迭代。
- 自動匯聚測試報告,根據評審問題類型進行分類,不斷改進Sonar檢查規則,從而形成良性循環。
6.2智能化代碼評審
- 提交代碼評審之后,通過AI大模型對代碼進行綜合評價,并給出建議。
- 通過智能代碼評審,產生評審報告,并進行智能化分析。