ここまでの実戦編では、機能を feature ブランチで作り、手元で git merge して main に取り込んできました。今回は手元のマージではなく、GitHub 上のプルリクエストを通します。
プルリクエストと聞くと「複数人のチームでやるもの」という印象があるかもしれません。でも、1 人のリポジトリでも使う価値があります。変更の差分が記録に残り、main を直接触らずにすむ。そして、細かいコミットを 1 つにまとめて履歴をきれいに保てます。第6回で仕組みを見たプルリクエストを、今度は自分のアプリで、本物の gh pr create から gh pr merge まで通してみます。
TL;DR
- 第10回のアプリに、タスクを「すべて / 未完了 / 完了」で絞り込むフィルタ機能を
featureブランチで実装します。 - これまでの手元マージではなく、本物のプルリクエストで取り込みます。
gh pr createで PR を作り、gh pr merge --squash --delete-branchでマージとブランチ削除まで一度に。 - 1 人のリポジトリでも PR を使う価値は、差分が記録に残ること、
mainが直接触られないこと、squash で履歴がきれいになることです。 --delete-branchはリモートだけでなくローカルのブランチも消します。最後にgit pullで手元のmainを最新にします。
今回やること――プルリクエストでフィルタ機能を足す
今日の流れは、GitHub flow と呼ばれる型です。第6回で図にしたものを、実際になぞります。
flowchart TD A["feature/filter-tabs で実装<br/>(ブランチを切る)"] --> B["push して<br/>Pull Request を作る"] B --> C["差分(Files changed)を<br/>自分でレビューする"] C --> D["squash でマージ<br/>+ブランチを自動削除"] D --> E["main が更新される<br/>git pull で手元も最新に"]
足すのは、タスクを「すべて / 未完了 / 完了」で切り替えるフィルタボタンです。機能としては小さいですが、これをローカルの git merge ではなくプルリクエスト経由で取り込むところが、今日の主役です。
AI に feature ブランチで実装させる
実装から PR、マージ、後片付けまで、流れごとひと言で頼みます。
タスクを「すべて / 未完了 / 完了」で絞り込めるボタンを付けて。新しいブランチ→ GitHub で Pull Request →自分でレビューOKでマージ→マージしたらブランチ削除、の流れで。
AI は feature/filter-tabs ブランチを切り、フィルタ機能を実装しました。中身でうまいと思ったのは、既存の機能にいっさい手を入れず、表示の切り替えだけで実現したことです。フィルタの本体は、この関数です。
let currentFilter = 'all';
// 現在のフィルタに合わせて各 li の表示を切り替える
function applyFilter() { list.querySelectorAll('li').forEach(function (li) { const isDone = li.classList.contains('done'); const visible = currentFilter === 'all' || (currentFilter === 'active' && !isDone) || (currentFilter === 'done' && isDone); li.style.display = visible ? '' : 'none'; });
}タスクを消したり作り直したりするのではなく、li.style.display を切り替えて隠すだけ。だから、これまでに作った完了チェック・削除・件数表示・localStorage 保存には、まったく影響しません。ボタン側は 3 つ並べて、押されたら currentFilter を切り替えて applyFilter() を呼ぶだけです。
<div class="filter-row" id="filter-row"> <button data-filter="all" class="active">すべて</button> <button data-filter="active">未完了</button> <button data-filter="done">完了</button> </div>
ここでも「指定した値」と「AI が勝手に決めた値」が分かれます。私が伝えたのは「すべて / 未完了 / 完了で絞り込むボタン」と「PR の流れで」だけ。次は AI が補いました。
- ブランチ名を
feature/filter-tabsにした - フィルタボタンを入力欄の直下に横並びで置き、押されている方を既存と同じ青 (
#4a90e2) にした - タスクを作り直さず
displayを切り替える、影響範囲の小さい実装方式を選んだ
「既存に影響を出さない実装」を選んだのは、地味ですが効く判断です。機能を足すたびに前の機能が壊れていないか不安になりますが、AI は最初から「足すだけ・既存は触らない」形に寄せてきました。
gh pr create で Pull Request を作る
実装できたら、feature/filter-tabs を push して、プルリクエストを作ります。AI は GitHub の公式コマンド gh でこれをやりました。
gh pr create --title "feat: タスクフィルタ機能を追加(すべて/未完了/完了)" --base main --head feature/filter-tabs
--base main は「main に取り込みたい」、--head feature/filter-tabs は「この feature ブランチの変更を」という指定です。実行すると、GitHub 上にプルリクエストが 1 つ作られ、URL が返ってきます。今回できたのは、これです。
https://github.com/ikuma-hiroyuki/todo-app-git-deep-dive/pull/1
このページの「Files changed」タブを開くと、feature/filter-tabs で加えた差分が、追加行・削除行の色つきで一覧できます。これがレビュー画面です。たとえ自分 1 人でも、変更をまとめて眺めて「よし、これで main に入れていい」と確認する。この儀式が、あとから「この機能はいつ、何を変えて入ったか」をたどれる記録になります。手元で git merge してしまうと、この一覧は残りません。
レビューして、squash でマージする
差分を確認して問題なければ、マージします。
gh pr merge 1 --squash --delete-branch
1 は PR の番号です。--squash と --delete-branch の 2 つが、今日のポイントです。
--squash は、feature ブランチにある複数のコミットを、main では 1 つのコミットにまとめて取り込みます。作業中の「とりあえず保存」みたいな細かいコミットが main の履歴に並ばず、「フィルタ機能を追加した」という 1 行だけが残ります。マージのしかたには 3 種類あって、この連載では squash を使います。
| 種別 | main の履歴 | ブランチのコミット |
|---|---|---|
| merge | マージコミットが 1 つ入る | そのまま全部残る |
| squash | 1 コミットにまとまる (この連載で使う) | 1 つにつぶされる |
| rebase | 直列に並べ直される | 書き換えられる |
個人の小さなアプリでは、機能 1 つ = main のコミット 1 つ、と読めるのがいちばん分かりやすいので、squash を選びます。マージ後の main の先頭は、こうなりました。
6865a3a feat: タスクを「すべて/未完了/完了」で絞り込むフィルタボタンを追加 (#1)
末尾の (#1) は、このコミットがプルリクエスト 1 番から来たことを示します。GitHub が自動で付けてくれるので、コミットから PR のレビュー画面へ後からたどれます。
マージ後の後片付け
--delete-branch を付けたので、役目を終えた feature/filter-tabs ブランチは、GitHub 側 (origin/feature/filter-tabs) が自動で削除されます。機能ごとにブランチを切って、マージしたら捨てる。第9回で触れたブランチの使い捨ての感覚が、PR でも同じように働きます。
ここで、ひとつ引っかかりがありました。念のため手元のブランチも消そうと思って git branch -d を試すと、エラーになったのです。
git branch -d feature/filter-tabs # error: branch 'feature/filter-tabs' not found.
理由は単純で、gh pr merge --delete-branch がリモートだけでなく手元のブランチも消していたからです。すでに無いものを消そうとしたので「見つからない」と言われた、という話で、実害はありません。こういう「一見エラーだけど実は問題ない」場面は、メッセージをそのまま AI に貼れば「これは --delete-branch がローカルも消したためで、問題ありません」と読み解いてくれます。
最後に、手元の main を最新にします。マージは GitHub 側で起きたので、手元の main はまだフィルタ機能の入る前のままです。
git pull
これで、手元の main も GitHub と同じ 6865a3a になりました。ブラウザでアプリを開いて、タスクを 3 件入れて 1 件を完了にし、フィルタを切り替えると、「すべて」で 3 件、「未完了」で 2 件、「完了」で 1 件と、ちゃんと絞り込めています。
AI に頼むときの言い方
今日も、指定した値と AI が勝手に決めた値に分かれていました。
| 種別 | こちらが指定した値 | 指定しないと AI が勝手に決める値 |
|---|---|---|
| 機能 | フィルタボタン (すべて / 未完了 / 完了)・PR の流れで | ブランチ名・UI の位置と色・実装方式 (display 切り替え) |
| PR | 取り込み先は main・マージ後にブランチ削除 | PR タイトルの文言・squash か merge か |
「PR の流れで」「マージしたらブランチ削除」という型だけ私が指定すれば、gh pr create のオプションや squash の選択は AI が埋めます。逆に、ブランチ名や PR タイトルのような「あとで自分が読むラベル」は、こだわりがあるなら最初に指定しておくと、後から探しやすくなります。
まとめ
第 11 回として、フィルタ機能を題材に、本物のプルリクエストを通しました。
- フィルタ機能を
feature/filter-tabsで実装した。AI は既存機能に影響しないdisplay切り替えを選んだ。 - 手元の
git mergeではなく、gh pr createでプルリクエストを作り、差分をレビューしてから取り込んだ。1 人でも、差分が記録に残り、mainが守られる。 gh pr merge --squash --delete-branchで、細かいコミットを 1 つにまとめ、役目を終えたブランチを自動で消した。--delete-branchは手元のブランチも消すので、git branch -dがnot foundになっても問題ない。- マージは GitHub 側で起きるので、最後に
git pullで手元のmainを最新にする。
プルリクエストは、チームの作法であると同時に、1 人の開発でも「変更を記録に残し、main を守る」ための道具です。GitHub flow の型──ブランチ→ PR → squash マージ→ブランチ削除→ git pull ──を一度通しておけば、人が増えても同じやり方がそのまま通用します。次回は、ダークモードと並び替えという 2 つの機能を、git worktree で同時並行に開発します。第7回で仕組みを見た worktree を、今度は AI を 2 体走らせる土台として使います。
なお、この記事で AI に渡したひと言は、すべて Claude(Sonnet) に実際に渡し、書いてあるとおりに動くことを確かめたものです。プルリクエストは実際に GitHub 上に #1 として作られ、squash マージまで通っています。モデルやその日の状態によって、AI が勝手に決める値 (ブランチ名や PR タイトルなど) は変わります。指定しなかった部分は変わりうる、と思って読んでください。
パイソンエンジニア部

