本記事はGitHub dockyard Advent Calendar 2024の最終日(25日目)の記事です
はじめに
こんにちは!大学生でWebフロントエンドエンジニアをしている、hibikiです:-)
先日同じくGitHub dockyard Advent Calendar 2024の11日目の記事としてCopilot code reviewを紹介しました!
コメントアウト等の自然言語をヒントにレビューを行っているなど、プレビューの機能ということもあり精度面で課題があるなあという印象でした🤔
Copilot code review (public preview)
先日記事を投稿してすぐ、Copilot code reviewのpublic previewへのがアクセスできるようになりました!
今回の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の無料版のリリースなど、広くユーザーにCopilotを利用してもらう基盤づくりをしている印象があります
VS Code Extensionだけでなく、github.comでもCopilotの顔を見ることが多くなりました👀
(github.comのCopilotメニューではCopilotが瞬きしたり左右を向いたりします)
開発者のそばに常に居続ける、まさにCopilot(副操縦士) のような存在になってきています!
How to Use
変更内容のレビュー
こちらは以前ウェイトリストに入っておらず紹介を断念した機能です!
アドベントカレンダーまでに間に合って良かったです☺️
画像中の赤で囲まれているアイコンをクリックすると、変更内容に対してCopilotコードレビューを行います
CopilotをPRレビュワーに設定する
試しにPR出してみよう!と思ったら早速出てきました👀
# Copilot code review (public preview)で引用した文章と同じ趣旨ですね
CopilotをレビュワーとしてPRを作成すると、作成直後にCopilotによるレビューが発生します
(上の例はREADME.md
のみの変更だったのでコメントは受けられませんでした)
新規PRに対してルールセットを用いた自動的なレビュー
上の例ではPRを出すときに毎回Copilotをレビュワーに設定する必要があります
ここでは、ルールセットを設定することでPRを出したときに自動的にCopilotによるレビューが走るようになります
手順
RepositoryのSettings -> Code and automation -> Rule -> Rulesetsから、"New branch ruleset"を選択します
下にスクロールして"Rules"に移り、"Require a pull request before merging"という項目をチェックします
"Request pull request review from Copilot (Preview)"の項目を選択することで、新規PRに対してCopilotからレビューを受けることができます!
実際に試してみた
前回同様、React+Viteのプロジェクトを使って試していきます!
今回は、前回のコードについて、コンポーネント化を行います
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;
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>
);
}
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>
);
}
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>
);
}
// 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>
);
}
// 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上でレビューを受けたところ、画面にこのような表示がありました
(スクリーンショットは別撮りで、1件レビューを受けました)
ポップアップ上の「ショーがスキップされました」ボタンをクリックすると、2件のレビューがありました
ひとつは関数名に関する提案ですね
関数や変数の命名などについてもレビューをしてくれるのは意外でした👀
もうひとつはaddTodo関数が空文字列を許容する事に関するレビューです
具体的な解決法は提示されていないものの、nullチェック忘れなどの初歩的なミスに気付けるのは大きいですね✨️
新規PRに対してルールセットを用いた自動的なレビュー
次に、変更内容をリモートリポジトリにアップロードしてPRを作成します!
新規PRを作成するときにCopilotがレビュワーとして自動的に設定されています!👀
毎回レビュワーに設定しなくていいのは便利ですね〜
CopilotをPRレビュワーに設定する
PRを作成するとCopilotのレビューが走り出します!
変更内容に応じてレビュー完了までの時間に差があるので気長に待ちましょう...
レビューの結果が出ました!
詳細もドロップダウンで確認できるのはありがたいですね✨️
このレビューでは不要な再レンダリングを防ぐためにuseCallback
を使用してtoggleTodo
をメモ化することを提案しています
レビューの結果をGitHub上に残しておけるため、Copilotのレビューを人間のレビュワーが判断する際に役立ちますね!
ぶっちゃけ精度はどう?
今回も前回同様まだまだ改善の余地はあると感じました!
コメントがレビューに大きく影響していたり、コードだけからコンテキストを読み取るまで至っていないのかなという印象です
(内部のモデルが気になりますね...🧐)
まとめ
今回は、Copilot code reviewのpublic previewの内容について触れました!
前回紹介した内容からより実用的に扱えるようになっており、広く浸透しそうだと感じました!
最近はgithub.com上でCopilotが使用されることが多くなりましたが、新機能が追加すればするほどとても使いやすくなっていきますね!
これからも期待して追っていきたいと思います👀
今回はGitHub dockyard Advent Calendar 2024の最終日を担当させていただきました!
アドベントカレンダー作成者の@kz_さん、アドベントカレンダー参加者の皆さま、またこれまでご清覧いただいた方々、誠にありがとうございました!🙇♂️