Copilot code reviewのpublic previewにアクセスできるようになった【GitHub dockyard】

本記事は[GitHub dockyard Advent Calendar 2024](https://qiita.com/advent-calendar/2024/github-dockyard)の最終日(25日目)の記事です

# はじめに

こんにちは！大学生でWebフロントエンドエンジニアをしている、hibikiです:-)

先日同じくGitHub dockyard Advent Calendar 2024の11日目の記事としてCopilot code reviewを紹介しました！

https://qiita.com/hibiki1023k/items/b839f62aeefb9e1818c0

コメントアウト等の自然言語をヒントにレビューを行っているなど、プレビューの機能ということもあり精度面で課題があるなあという印象でした🤔
# Copilot code review (public preview)
<!-- public previewにできることを書く -->
先日記事を投稿してすぐ、Copilot code reviewのpublic previewへのがアクセスできるようになりました！

https://x.com/hibi_dev/status/1867381651787198512

今回のpublic previewでは、次のようなことが書かれています
> Copilot code review gives you fast, actionable AI feedback on your code so you can start iterating towards "ready to merge" while you wait for a human reviewer.

レビュワーの負担を減らすため、Copilotを使った繰り返しのレビュー機能を取り入れています

機能としては次の3つが挙げられています
* VS Code(ソース管理タブ)で変更内容のレビューができる
* github.comのプルリクエスト(以下、PR)のレビュワーにCopilotをタグ付けできる
* github.comの新規PRに対してルールセットを用いた自動的なレビューができる


直近では[VS Code上でのGitHub Copilotの無料版のリリース](https://github.blog/news-insights/product-news/github-copilot-in-vscode-free/)など、広くユーザーにCopilotを利用してもらう基盤づくりをしている印象があります
VS Code Extensionだけでなく、github.comでもCopilotの顔を見ることが多くなりました👀
(github.comのCopilotメニューではCopilotが瞬きしたり左右を向いたりします)

開発者のそばに常に居続ける、まさに**Copilot(副操縦士)** のような存在になってきています！

# How to Use

## 変更内容のレビュー
<!-- 現状のテストコードをコンポーネントごとに分離して実装 -->
こちらは以前ウェイトリストに入っておらず紹介を断念した機能です！
アドベントカレンダーまでに間に合って良かったです☺️
![VS Codeのソース管理タブのソース管理メニュー。README.mdとApp.tsxが変更されている。メニュー上部にある吹き出しの中にタグが入ったアイコンが赤く囲まれている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/8a676531-1d43-a49d-884f-aaaef6609ad8.png)
画像中の赤で囲まれているアイコンをクリックすると、変更内容に対してCopilotコードレビューを行います

## CopilotをPRレビュワーに設定する
<!-- 新規機能を実装してPRを出す -->
試しにPR出してみよう！と思ったら早速出てきました👀
![Reviews欄にCopilotが提案され、"Copilot can review pull requests", "Request a review from Copilot to get fast, actionable feedback on your code, so you can start iterating before you receive a human review"という文章と"Learn more"と"OK, dismiss"の2つのボタンが表示されている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/15c68f9f-da11-bda9-a9fd-f95f0d18aeea.png)
[# Copilot code review (public preview)](#copilot-code-review-public-preview)で引用した文章と同じ趣旨ですね

CopilotをレビュワーとしてPRを作成すると、作成直後にCopilotによるレビューが発生します
!["doc: Update README.md"というPRのレビュワーにCopilotを設定し、Copilotが"Copilot reviewed 1 out of 1 changed files in this pull request and generated no comments."とコメントしている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/7dbf7c80-478a-8cb6-77b8-cc759daef30a.png)
(上の例は`README.md`のみの変更だったのでコメントは受けられませんでした)

## 新規PRに対してルールセットを用いた自動的なレビュー
<!-- 上と同様のPRについて試す -->
上の例ではPRを出すときに毎回Copilotをレビュワーに設定する必要があります
ここでは、ルールセットを設定することでPRを出したときに自動的にCopilotによるレビューが走るようになります

### 手順

RepositoryのSettings -> Code and automation -> Rule -> Rulesetsから、"New branch ruleset"を選択します
下にスクロールして"Rules"に移り、"Require a pull request before merging"という項目をチェックします
![Rules, Branch Rulesが縦に並んでいる。"Require a pull request before merging"という項目を赤く囲んでいる](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/6be1736b-862a-0204-8702-289e5449eb11.png)

"Request pull request review from Copilot (Preview)"の項目を選択することで、新規PRに対してCopilotからレビューを受けることができます！
!["Require a pull request before merging"の項目を選択したときにいくつかの項目が出現している。"Request pull request review from Copilot*(Preview)"の項目が赤く囲まれ、チェックが付いている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/6425a809-fd24-f409-23ee-437b4aad6073.png)

# 実際に試してみた
前回同様、React+Viteのプロジェクトを使って試していきます！
今回は、前回のコードについて、コンポーネント化を行います

```tsx:App.tsx
import { useState } from 'react';
import './index.css';
import SearchBar from './components/SearchBar';
import FilterButtons from './components/FilterButtons';
import TodoStats from './components/TodoStats';
import TodoList from './components/TodoList';
import AddTodo from './components/AddTodo';

// Todoインターフェースを定義します。これは個々のタスクを表します。
interface Todo {
  id: number; // タスクの一意のID
  text: string; // タスクのテキスト
  completed: boolean; // タスクが完了したかどうか
}

// Appコンポーネントを定義します。これはアプリケーションのメインコンポーネントです。
function App() {
  // タスクの状態を管理するためのuseStateフック
  const [todos, setTodos] = useState<Todo[]>([]);
  // 入力テキストの状態を管理するためのuseStateフック
  const [inputText, setInputText] = useState('');
  // 検索テキストの状態を管理するためのuseStateフック
  const [searchText, setSearchText] = useState('');
  // フィルターの種類の状態を管理するためのuseStateフック
  const [filterType, setFilterType] = useState<'all' | 'active' | 'completed'>('all');

  // 新しいタスクを追加する関数
  const addTodo = () => {
    // 入力テキストが空白の場合は何もしない
    if (inputText.trim() === '') return;
    // 新しいタスクを作成
    const newTodo: Todo = {
      id: Date.now(), // 現在の日時をIDとして使用
      text: inputText, // 入力テキストをタスクのテキストとして使用
      completed: false // 新しいタスクは未完了として設定
    };
    // タスクの配列に新しいタスクを追加
    setTodos([...todos, newTodo]);
    // 入力テキストをリセット
    setInputText('');
  };

  // タスクの完了状態を切り替える関数
  const toggleTodo = (id: number) => {
    // タスクの配列をマップして、指定されたIDのタスクの完了状態を切り替える
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // フィルターとソートされたタスクの配列を取得する関数
  const getFilteredAndSortedTodos = () => {
    // タスクの配列をコピー
    let result = [...todos];
    // フィルターの種類に応じてタスクをフィルタリング
    if (filterType === 'active') {
      result = result.filter(todo => !todo.completed);
    } else if (filterType === 'completed') {
      result = result.filter(todo => todo.completed);
    }
    
    // 検索テキストに一致するタスクをフィルタリング
    result = result.filter(todo =>
      todo.text.toLowerCase().includes(searchText.toLowerCase())
    );

    // タスクをIDの降順でソート
    return result.sort((a, b) => b.id - a.id);
  };

  // タスクの統計情報を計算
  const stats = {
    total: todos.length, // 全タスクの数
    completed: todos.filter(t => t.completed).length, // 完了したタスクの数
    remaining: todos.filter(t => !t.completed).length // 残りのタスクの数
  };

  // フィルターとソートされたタスクの配列を取得
  const filteredTodos = getFilteredAndSortedTodos();

  return (
    // アプリケーションのメインコンテナ
    <div className="w-full h-full p-8">
      {/* アプリケーションのコンテンツを表示するためのdiv要素 */}
      <div style={{ width: '100%', maxWidth: '42rem', margin: '0 auto', backgroundColor: 'white', borderRadius: '0.5rem', borderWidth: '2px' }}>
        {/* 検索バー、フィルターボタン、タスク追加フィールドを表示するためのdiv要素 */}
        <div className="p-4 border-b-2">
          <SearchBar
            searchText={searchText} // 検索テキストの値
            setSearchText={setSearchText} // 検索テキストを設定する関数
          />
          <FilterButtons filterType={filterType} setFilterType={setFilterType} />
          <AddTodo
            inputText={inputText} // 入力テキストの値
            setInputText={setInputText} // 入力テキストを設定する関数
            addTodo={addTodo} // タスクを追加する関数
          />
        </div>
        {/* タスクの統計情報を表示するためのdiv要素 */}
        <div className="p-4 border-b-2 text-sm text-gray-500">
          全て: {stats.total} | 完了: {stats.completed} | 残り: {stats.remaining}
        </div>
        {/* タスクのリストを表示 */}
        <TodoList todos={filteredTodos} toggleTodo={toggleTodo} />
      </div>
    </div>
  );
}

// Appコンポーネントをエクスポート
export default App;
```
```tsx:AddTodo.tsx
import React from 'react';

// Propsインターフェースを定義します。これはコンポーネントに渡されるプロパティを保持します。
interface Props {
  inputText: string; // 入力テキスト
  setInputText: React.Dispatch<React.SetStateAction<string>>; // 入力テキストを設定する関数
  addTodo: (e: React.KeyboardEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement>) => void; // タスクを追加する関数
}

// AddTodoコンポーネントを定義します。これは新しいタスクを追加するための入力フィールドとボタンを表示します。
export default function AddTodo({
  inputText,
  setInputText,
  addTodo
}: Props) {
  return (
    // 新しいタスクを追加するための入力フィールドとボタンを表示するためのdiv要素
    <div className="flex gap-2">
      {/* 新しいタスクを入力するためのinput要素 */}
      <input
        className="flex-1 h-12 px-4 border-2 rounded-lg"
        placeholder="新しいタスク" // プレースホルダーとして「新しいタスク」を表示
        value={inputText} // 入力テキストの値
        onChange={(e) => setInputText(e.target.value)} // 入力テキストが変更されたときに呼び出される関数
        onKeyPress={(e) => {
          if (e.key === 'Enter') {
            addTodo(e); // Enterキーが押されたときにタスクを追加
          }
        }}
      />
      {/* タスクを追加するためのボタン */}
      <button
        style={{ padding: '0 1.5rem', backgroundColor: '#3b82f6', color: 'white', borderRadius: '0.5rem' }}
        onClick={addTodo} // ボタンがクリックされたときにタスクを追加
      >
        追加
      </button>
    </div>
  );
}
```
```tsx:FilterButtons.tsx
import React from 'react';

// Propsインターフェースを定義します。これはコンポーネントに渡されるプロパティを保持します。
interface Props {
  filterType: 'all' | 'active' | 'completed'; // フィルターの種類
  setFilterType: React.Dispatch<React.SetStateAction<'all' | 'active' | 'completed'>>; // フィルターの種類を設定する関数
}

// FilterButtonsコンポーネントを定義します。これはフィルターボタンを表示します。
export default function FilterButtons({ filterType, setFilterType }: Props) {
  return (
    // フィルターボタンを表示するためのdiv要素
    <div className="flex gap-2 mb-4">
      {/* 全てのタスクを表示するボタン */}
      <button
        className={`px-4 py-2 rounded ${filterType === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
        onClick={() => setFilterType('all')} // フィルターを「全て」に設定
      >
        全て
      </button>
      {/* 未完了のタスクを表示するボタン */}
      <button
        className={`px-4 py-2 rounded ${filterType === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
        onClick={() => setFilterType('active')} // フィルターを「未完了」に設定
      >
        未完了
      </button>
      {/* 完了済みのタスクを表示するボタン */}
      <button
        className={`px-4 py-2 rounded ${filterType === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
        onClick={() => setFilterType('completed')} // フィルターを「完了済み」に設定
      >
        完了済み
      </button>
    </div>
  );
}
```
```tsx:SearchBar.tsx
import React from 'react';

// Propsインターフェースを定義します。これはコンポーネントに渡されるプロパティを保持します。
interface Props {
  searchText: string; // 検索テキスト
  setSearchText: React.Dispatch<React.SetStateAction<string>>; // 検索テキストを設定する関数
}

// SearchBarコンポーネントを定義します。これは検索バーを表示します。
export default function SearchBar({
  searchText,
  setSearchText,
}: Props) {
  return (
    // 検索バーを表示するためのdiv要素
    <div>
      {/* 検索テキストを入力するためのinput要素 */}
      <input
        className="w-full h-12 px-4 mb-4 border-2 rounded-lg"
        placeholder="検索" // プレースホルダーとして「検索」を表示
        value={searchText} // 検索テキストの値
        onChange={(e) => setSearchText(e.target.value)} // 検索テキストが変更されたときに呼び出される関数
      />

    </div>
  );
}
```
```tsx:TodoList.tsx
// Todoインターフェースを定義します。これは個々のタスクを表します。
interface Todo {
  id: number; // タスクの一意のID
  text: string; // タスクのテキスト
  completed: boolean; // タスクが完了したかどうか
}

// Propsインターフェースを定義します。これはコンポーネントに渡されるプロパティを保持します。
interface Props {
  todos: Todo[]; // タスクの配列
  toggleTodo: (id: number) => void; // タスクの完了状態を切り替える関数
}

// TodoListコンポーネントを定義します。これはタスクのリストを表示します。
export default function TodoList({ todos, toggleTodo }: Props) {
  return (
    // タスクのリストを表示するためのul要素
    <ul className="divide-y">
      {/* タスクの配列をマップしてli要素を生成 */}
      {todos.map(todo => (
        // 個々のタスクを表示するためのli要素
        <li
          key={todo.id} // タスクの一意のIDをキーとして使用
          className="flex items-center p-4 hover:bg-gray-50 cursor-pointer"
          onClick={() => toggleTodo(todo.id)} // タスクの完了状態を切り替える
        >
          {/* タスクの完了状態を表示するためのチェックボックス */}
          <input
            type="checkbox"
            checked={todo.completed} // タスクが完了しているかどうか
            className="w-5 h-5 mr-4"
            readOnly
          />
          {/* タスクのテキストを表示 */}
          <span className={`flex-1 ${todo.completed ? 'line-through text-gray-400' : ''}`}>
            {todo.text}
          </span>
        </li>
      ))}
    </ul>
  );
}
```
```tsx:TodoStats.tsx
// Statsインターフェースを定義します。これはタスクの統計情報を保持します。
interface Stats {
  total: number; // 全タスクの数
  completed: number; // 完了したタスクの数
  remaining: number; // 残りのタスクの数
}

// Propsインターフェースを定義します。これはコンポーネントに渡されるプロパティを保持します。
interface Props {
  stats: Stats; // タスクの統計情報
}

// TodoStatsコンポーネントを定義します。これはタスクの統計情報を表示します。
export default function TodoStats({ stats }: Props) {
  return (
    // 統計情報を表示するためのdiv要素
    <div className="p-4 border-b-2 text-sm text-gray-500">
      {/* 全てのタスクの数を表示 */}
      全て: {stats.total} | 
      {/* 完了したタスクの数を表示 */}
      完了: {stats.completed} | 
      {/* 残りのタスクの数を表示 */}
      残り: {stats.remaining}
    </div>
  );
}
```
フロントエンドのみで完結させるため、グローバルな状態はApp.tsxで保持しています

## 変更内容のレビュー
<!-- 現状のテストコードをコンポーネントごとに分離して実装 -->
![VS Code上にポップアップが表示されている。「コードを確認しても、フィードバックは表示されませんでした。」というテキストの下に、「ショーがスキップされました」と「キャンセル」のふたつのボタンがある](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/8d4265c8-ba2b-5338-54c2-b192fd307b39.png)
VS Code上でレビューを受けたところ、画面にこのような表示がありました
(スクリーンショットは別撮りで、1件レビューを受けました)

ポップアップ上の「ショーがスキップされました」ボタンをクリックすると、2件のレビューがありました
!["The function name 'getFilteredAndSortedTodos' could be more descriptive. Consider renaming it to 'filterAndSortTodos'."とCopilotからレビューを受けている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/e552fcab-4b41-e8bd-aeae-87e4ce709ac9.png)
ひとつは関数名に関する提案ですね
関数や変数の命名などについてもレビューをしてくれるのは意外でした👀

!["The 'addTodo' function should be tested to ensure it handles edge cases, such as adding a todo with only whitespace."とCopilotからレビューを受けている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/67749f8a-d5c7-005f-58e1-dac11c27a9b7.png)
もうひとつはaddTodo関数が空文字列を許容する事に関するレビューです
具体的な解決法は提示されていないものの、nullチェック忘れなどの初歩的なミスに気付けるのは大きいですね✨️

## 新規PRに対してルールセットを用いた自動的なレビュー
<!-- 上と同様のPRについて試す -->
次に、変更内容をリモートリポジトリにアップロードしてPRを作成します！
![PRを作成する際の画面。Copilotがレビュワーとして選択されている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/875e09d8-1d52-c47e-3697-0f9ff48731ac.png)
新規PRを作成するときにCopilotがレビュワーとして自動的に設定されています！👀
毎回レビュワーに設定しなくていいのは便利ですね〜

## CopilotをPRレビュワーに設定する
<!-- 新規機能を実装してPRを出す -->
PRを作成するとCopilotのレビューが走り出します！
変更内容に応じてレビュー完了までの時間に差があるので気長に待ちましょう...

![Copilotのレビューのリクエストが完了した旨のステータスとレビューの結果が表示されている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3676543/570c79b9-26db-fb2d-4fc7-32b3cd5a4aa8.png)
レビューの結果が出ました！
詳細もドロップダウンで確認できるのはありがたいですね✨️

このレビューでは不要な再レンダリングを防ぐために`useCallback`を使用して`toggleTodo`をメモ化することを提案しています
レビューの結果をGitHub上に残しておけるため、Copilotのレビューを人間のレビュワーが判断する際に役立ちますね！

# ぶっちゃけ精度はどう？
今回も前回同様まだまだ改善の余地はあると感じました！
コメントがレビューに大きく影響していたり、コードだけからコンテキストを読み取るまで至っていないのかなという印象です
(内部のモデルが気になりますね...🧐)

# まとめ
今回は、Copilot code reviewのpublic previewの内容について触れました！
前回紹介した内容からより実用的に扱えるようになっており、広く浸透しそうだと感じました！

最近はgithub.com上でCopilotが使用されることが多くなりましたが、新機能が追加すればするほどとても使いやすくなっていきますね！
これからも期待して追っていきたいと思います👀

今回はGitHub dockyard Advent Calendar 2024の最終日を担当させていただきました！

アドベントカレンダー作成者の@kz_さん、アドベントカレンダー参加者の皆さま、またこれまでご清覧いただいた方々、誠にありがとうございました！🙇‍♂️