在VS Code中Code Review的实践

2,331 阅读12分钟

在之前的文章《ChatGPT 加持下的 Code Review 探索》中,介绍了咱们团队内使用了VS Code来进行code review。为什么开发这样一款插件,包老师的文章也介绍了下面两点原因

Reviewer 需要切换到 Web 浏览器中进行 Review,而 Web 浏览器中的代码展示风格、样式等等和自己的 IDE 中的偏好设置并不一样,这会影响 Reviewer 看代码的效率。

如果 Reviewer 觉得某段代码可以优化,但自己也没有把握该如何优化,自己提供的代码也需要测试验证,所以他一般会需要这么操作:把代码拷贝到自己所使用的 IDE 中,优化完代码后,再粘贴到 Merge Request 中的评论中,这个过程是比较繁琐的。不管如何,编码这个环节肯定是在自己最熟悉的 IDE 中进行才是最高效的,比如还可以使用 Github Copilot 插件对变更代码进行优化。

本节我将介绍这个插件的具体实现。

先来看插件的整体布局 image.png

需要实现的功能主要如下

  • 账号登录
  • 分类MR列表展示
  • 点击左侧MR中的变更文件能够在右侧editor区域展示对应文件更改对比、评论操作
  • 在下方区域中显示对应修改的ChatGPT分析报告
  • reviewer收到review通知后能够快速打开vscode进行reviewer

相信不少同学使用过GitHub Pull RequestsGitlab workflow插件,这两个插件都能够显示MR的变更,刚开始我们也想用这个插件,经过调研,结果很遗憾,这两个插件都不适用于我们团队

  • 账号只能登录登录官方网站的账号,我们公司的是内部私有的仓库
  • 分类展示也只能显示当前打开项目的MR,我们review同事的代码不可能说需要打开指定的项目吧,这样review也太麻烦了,不符合我们提效的目标
  • 无法做到定制内容,例如:显示区域内容,我们需要显示ChatGPT分析报告(分析报告存放于另一个系统)、链接打开mr变更、copilot分析等

基于如上述求,那就我们自己写一个符合团队需求的插件,下面我将介介绍插件的部分功能的实现。

可能部分同学没开发过vscode插件,这没关系,如果你是前端同学,入门也相当的容易,推荐查看

插件基本配置

首先我们要在activeBar中声明一个应用容器ysfWorkbench,然后给ysfWorkbench中绑定一个viewmergeRequest

package.json

  ...
  + "contributes": {
  +    "viewsContainers": {
  +        "activitybar": [
  +            {
  +                "id": "ysfWorkbench", // 这里是容器id,后面绑定视图需要使用
  +                "title": "Ysf Workbench", 
  +                "icon": "assets/images/ys.png"
  +            }
  +        ]
  +    },
  +"views": {
  +    "ysfWorkbench": [ // 将view绑定到上面的容易中
  +        {
  +            "id": "mergeRequest", // 在后面view显示的内容会用到
  +            "name": "Ysf Workbench",
  +            "default": true
  +        }
  +    ],
  + }
  ...

登录

在拉取MR数据之前,我们需要登录账号,vscode贴心的为我们提供了未登录的占位内容,用于显示登录内容,只需要在package.json中加入viewsWelcome, 当vscode contextysf.gitlab:noAccount不存在时显示占位内容

package.json

...
contributes: {
 + "viewsWelcome": [
 +  {
 +   "view": "mergeRequest",
 +   "contents": "欢迎使用vscode插件code review, 由于您还未登录gitlab账号,请先登录\n [立即登录](command:ysf.gitlab:authenticate)",
 +   "when": "ysf.gitlab:noAccount" // 读取`context`中的`ysf.gitlab:noAccount`值
 +  }
 + ],
}
...

此时,用户点击立即登录按钮,即可执行ysf.gitlab:authenticate命令,登录gitlab账号

vscode.commands.registerCommand('ysf.gitlab:authenticate', login)),

唤起登录

登录采用gitlab OAuth进行认证,首先我们需要申请一个Applications,具体操作可可查看login函数中拼接好url,然后通过window.open打开链接。格式大概如下

extension.ts

function login() {
  const REDIRECT_URI = 'vscode://ysf.ysf-vsc-plugin/authentication'
  const APP_ID = '803a04de757168037e1e6c87q3cb4b3ea881e4b52bde9fa1819b3b6d02c68b4d'
  window.open(`https://gitlab.example.com/oauth/authorize?client_id=${APP_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&state=STATE&scope=${REQUESTED_SCOPES}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256`)
}

随后打开浏览器跳转到gitlab进行授权,授权完成后会自动打开vscode(就是浏览器会根据打开链接REDIRECT_URI),此时gitlab会拼接上授权code={xxx}打开vscode,格式大概如下

window.location.href='vscode://ysf.ysf-vsc-plugin/authentication?code=311f08d9c2f5eb7328502764c6a055ade9758c3432403e0a895694ea7a78e372'

vscode 验证登录

浏览器在打开浏览器的时候,会带上路径/authentication,跟我们的路由是不是很像,那我们怎么取拿到这个路径信息呢?vscode也很贴心的为我们提供了registerUriHandler这个方法进行监听 我们只需要稍微加点代码

// 实现一个UriHandle实例
class GitLabUriHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
  async handleUri(uri: vscode.Uri): Promise<void> {
    this.fire(uri); 
  }
}

const gitlabUriHandler = new GitLabUriHandler();
// 注册uri监听
vscode.window.registerUriHandler(gitlabUriHandler);

// 注册监听回调,当打开vscode时,会自执行gitlabUriHandler.handleUri()
gitlabUriHandler.event((uri) => {
   const { path, query, code } = uri;
   if (path === '/authentication') { // 这个路径说明是登录
      // 在这里进行登录验证,通过code,调用github登录接口 
      // 具体可查看https://docs.gitlab.com/ee/api/oauth2.html
      // 请求api https://gitlab.example.com/oauth/token 验证登录
      // .......此处省略888行代码.....
      const accessToken = //.... 通过code换取的凭证
    
      if (accessToken) {
        // 设置插件上下文,表示已登录账号,这个时候 `mergeRequest`会显示成 treeView视图
        // 执行setContext会刷新插件视图,类似于react中的setState,显示不同的内容
        vscode.commands.executeCommand('setContext', 'ysf.gitlab:noAccount', false); 
      }
    }
})

MR分类列表

注册treeView

接下来我们需要在extension.ts中注册一个treeView(什么是treeView), extension.ts

// 注册树形视图

vscode.window.registerTreeDataProvider('mergeRequest', mergeRequestProvider);

mergeRequestProvider中只需要实现TreeDataProvider中的方法

class MergeRequestProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
  // 主要实现下面两个方法
  getChildren(element) { // 用户获取子元素
    // 这里编写获取子集的代码
    // 这里可参考官方的例子
    return [
      // 例如 new ChangeFile(mr) // mr信息
    ]
  }
  getTreeItem(element) { // 用于获取树形视图中的元素的图标、文本和其他显示属性。这个方法负责返回树形视图中各个元素的显示信息
    return element // 这里可做更改
  }
}

const mergeRequestProvider = new MergeRequestProvider():

class Item extends vscode.TreeItem {
  constructor(label) {
    super(label) // 还可以传入其他属性,tooltip,description等
  }
  iconPath = { //标题前的图标,
    dark: // 深色图标路径
    light: // 亮色图标路径
  }
}

export default mergeRequestProvider; // 导出treeData实例

获取MR列表、MR文件变更列表

在上面的代码中,我们注册了显示的treeDataprovider,我们可以在getChildren中编写异步代码,比如我们请求分支列表数据分支的变更文件数据,下面是获取变更文件的例子 接口

// 假设已经在接口获取了MR的数据,我们就从接口中拉取变更文件数据

class ChangeFile {
  constructor(mr) {
    this.mr = mr; // mr信息,包行 mr_iid, project_id
  }
  async getChildren(element) { // 获取files
    const {changes} = fetchMrDetail(mr) // 通过mr_iid, project_id获取数据,   https://docs.gitlab.com/ee/api/merge_requests.html#get-single-merge-request-changes
    // diffs.map(diff => {
      return FileItemModal(changes); // cha
  })
  ....
}

changes 有如下属性

 [{
    "old_path": "VERSION",
    "new_path": "VERSION",
    "a_mode": "100644",
    "b_mode": "100644",
    "diff": "@@ -1 +1 @@\ -1.9.7\ +1.9.8",
    "new_file": false,
    "renamed_file": false,
    "deleted_file": false
}]

点击文件显示变更

当点击最后一级文件时,我们要打开一个diff的窗口,vscode提供了内置命令vscode.diff,我们在最后一级绑定一个command, vscode.diff需要传入三个参数 1.老文件的uri 2.新文件的uri 3.title

代码如下

export class ChangedFileModal extends vscode.TreeItem {
 constructor(options: { change, mr, mrVersion}) { // mrVersion
  const { change, mr, mrVersion } = options;
  super(vscode.Uri.file(file.new_path));
  this.description = file.new_path;

  const uris = getBaseAndHeadUri(mr, mrVersion, file); // 构建vscode需要的uri,这里会加入query信息,包括mr_iid, project_id、commitId等信息,方便在后续用到
  this.headFileUri = uris.headFileUri;
  this.baseFileUri = uris.baseFileUri;
  this.command = {  // 绑定命令
   title: 'Show changes',
   command: VS_COMMANDS.DIFF, // 命令为`vscode.diff`
   arguments: [
    this.baseFileUri,
    this.headFileUri,
    `${path.basename(change.new_path)} (!${mr.iid})`,
   ],
  };
 }
}

注册文件系统

在你点击文件名想查看对饮的文调用文件对比命令时,此时你会发现vscode报错了 diff报错

这是因为vscode默认是读取本地文件,然而我们本地没有这些文件,vscode没有提供通过远程文件读取文件内容的接口,但是为我们提供了自定义文件系统的provider:registerFileSystemProvider

  // 注册文件review
  vscode.workspace.registerFileSystemProvider(
   REVIEW_URI_SCHEME, // 协议名称,自定义,文件系统标识
   new ReviewFileSystem(),
   ReviewFileSystem.OPTIONS, // 传入为非只读模式
  );
  // 官方提供的ReadOnlyFileSystem,需要实现readFile来获取文件内容、stat获取文件信息
  class ReviewFileSystem extends ReadOnlyFileSystem {
    static OPTIONS: RegisterOptions = {
      isReadonly: false,  // 是否只读,这个在我们调用copilot有用
    };

     // 获取文件
     // 如果是老变更文件这传入是vscode.diff的第一个参数,即arguments[2]
     // 如果是新变更文件这传入是vscode.diff的第二个参数,即arguments[1]
    async readFile(uri: vscode.Uri): Promise<Uint8Array> {
      const params = fromReviewUri(uri); // 这里会获取一些query信息,例如r_iid, project_id、commitId等
      if (isEmptyFileUri(uri)) return new Uint8Array();

      return this.#readFileRemote(params); // 获取远程文件内容,需要转换成Buffer Uint8Array
    }
    async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
      let size: number;

      const params = fromReviewUri(uri);
      if (isEmptyFileUri(uri)) {
        size = 0;
      } else {
        size = await this.#sizeRemote(params); // 这里也会调用this.#readFileRemote获取文件打大小
      }
      return {
        type: vscode.FileType.File,
        ctime: Date.now(),
        mtime: Date.now(),
        size,
      };
    }
  }

至此,文件内容已经可以显示在editor中,注意这里会调用4次获取文件内容,有两次获取原变更的内容和大小,有两次获取新变更的内容和大小 image.png

通过popo快速打开MR进行review

在开发上面的功能登录认证中我们通过监听/authentication来完成用户登录验证,给我一个思考:我们已经有popo通知,能不能通过popo通知来唤起vscode来进行review,这样就不用来查找自己需要review的MR? 经过调研,虽然我们无法通过popo直接打开vscode,但是我们可以通过http链接打开浏览器,利用浏览器这个中间人来打开vscode

于是在通知中加上了如下信息

vscode查看:https://gate.netease.com/workbench/api/workbench/public/vsc?projectId=110576&mrIid=161

访问这个链接后会用过js打开vscode

<script>
  window.location.href = 'vscode://ysf.ysf-vsc-plugin/mr?projectId=110576&mrIid=161';
</script>

显示popo进入的MR视图,然后在popoEnterMrProvider中实现获取MR的信息,更分类获取MR信息处理一样

gitlabUriHandler.event((uri) => {
   const { path, query, code } = uri;
   if (path === '/authentication') { // 这个路径说明是登录
   ...
   }
   + if (path === 'mr') {
   +  // 这里我们在显示MR列表中再加一个view,当`path === 'mr'`时显示这个view,同时我们在注册一个`treeDataProvider:popoEnterMrProvider`
   +  // 存储地址栏的参数信息
   +  // 执行setContext会刷新插件视图,类似于react中的setState,显示不同的内容
   +  // 同时可以做些优化:比如选中第一个文件
   +   vscode.commands.executeCommand('setContext', 'ysf.mr.popo.enter', true); 
   + }
})

在package.json中声明一个view,

  ...
  views": {
     "ysfWorkbench": [ // 将view绑定到上面的容易中
         {
             "id": "mergeRequest", // 在后面view显示的内容会用到
             "name": "Ysf Workbench",
             "default": true
         },
        + {
        +     "id": "popoEnterMR",
        +     "name": "POPO链接进入的MR",
        +     "default": true
        +     "when": "ysf.mr.popo.enter && !ysf.gitlab:noAccount", // 当ysf.mr.popo.enter存在时会显示改view
        + }
     ],
  }
  ...

效果如下 image.png

显示ChatGPT分析报告

由于我们的web系统中已经存放了分析报告,所以在vscode只要能展示出来即可,这里使用webview显示在editor下方是个不错的选择 在下方面板中开辟一个tab出来用于显示我们的报告

  "contributes": {
      "viewsContainers": {
          "activitybar": [
            ...
          ],
          "panel": [
              {
                  "id": "ysfReportPanel", // 这里声明一个面板
                  "title": "ChatGPT分析报告",
                  "icon": "assets/images/ys.png"
              }
          ]
      },
  }

extension.ts中注册webview,显示咱们报告信息,当文件切换时通知webview页面刷新报告

vscode.window.registerWebviewViewProvider(ysfReportPanel, {
  resolveWebviewView: (webviewView) => {
     webviewView.webview.options = {...}; // 一些配置
   webviewView.title = 'ChatGPT分析报告';
   webviewView.webview.html = '....' // 这里是我们的html页面,一般是经过webpack打包后的html文件的内容
   webviewView.webview.onDidReceiveMessage((message) => {
        // 这里主要是和webview页面进行通信
        // 例如当查看的文件变化时,通知页面重新请求对应的报告
        // 注册当活动文件变化时的回调函数, 通知webview文件发生变化
        const editorDisposable = vscode.window.onDidChangeActiveTextEditor(async (editor) => {
          if (editor) {
            // 获取当前文件的 URI
            const fileUri = editor.document.uri;
            if (fileUri.scheme === REVIEW_URI_SCHEME) {
              webviewView.webview.postMessage({
                command: messageCommand.REPORT_FILE_CHANGED, // 通知文件改变了,在webview页面中通过 window.addEventListener('message', callback)
                data: fromReviewUri(fileUri),
              });
            }
          }
        });
      })
  }
})

效果如下图 显示ChatGPT报告

copilot功能增强

使用copilot帮助阅读代码

前文提到,我们在系统默认的文件对比视图中是无法使用copilot右键功能的,这是因为在copilot源码中禁用了相关功能,当识别到只读模式后是无法显示的 image.png 那我们自定义的文件系统就排上用场了,只需要将isReadonly 传入false,我们的copilot就可以正常使用

  vscode.workspace.registerFileSystemProvider(
   REVIEW_URI_SCHEME, // 协议名称,自定义,文件系统标识
   new ReviewFileSystem(),
   {isReadonly : false}, // 传入为非只读模式
  );

使用copilot帮助优化代码

当我们review一段代码的时候我们觉得很有优化空间,希望借助copilot快速帮我们想到优化方案,这个时候可能我们会把这部分代码拷贝到chat窗口输入,这其实也是一个高频重复且繁琐的事,有没有快捷的方法呢? image.png 我通过查看插件的代码(copilot插件代码没开源,这里可以直接找到插件的安装目录,然后直接阅读里面的代码,里面是写编译后的代码,但是没关系,我们只需要先格式化代码,还是能看出些大概得)。在我们点击explain thisimage.pnggithub copilot的源码里是调用了一个命令github.copilot.interactiveEditor.explain,那么我们也可以在自己的插件里添加一个右键菜单。加一个命定来调用这个copilot的命令,实现如下

vscode.commands.registerCommand(`ysf.copilot.ptimize`, () => {
const editor = vscode.window.activeTextEditor;
	if (!editor) {
		return;
	}
	const selection = editor.selection;
	const text = editor.document.getText(selection);
	// 获取当前文件的后缀名
	const languageId = editor.document.languageId;
	// 尽可能列举下所有语言
	const languageMap: Record<string, string> = {
		js: 'js',
		ts: 'ts',
        ... // 用于映射对应的语言,以便copilot识别更精准
	};
	const language = languageMap[languageId] || 'js';
    
	vscode.commands.executeCommand('github.copilot.interactiveEditor.explain', `@workspace 帮我优化下面这段代码 \n\n \`\`\` ${language} \n${text} \n\`\`\` \n\n`);
})

这个插件还可以进行代码评论,这部分功能如果有感兴趣的同学,我后面再专门关于如何在vscode评论的文章,这里我就不介绍了

以上是我们在vscode review的一些实践,希望能对大家有所启发。