--patch オプションとは
git add の --patch
または -p
オプションを使うことで、ファイル内の変更をハンクごとにステージングできます。ハンクとは変更の塊のようなもので、git diff
を実行すると表示される差分の粒度でもあります。以下の例では、Git が変更を 1 つのハンクとして認識しています。
diff --git a/src/lib/sample.ts b/src/lib/sample.ts
index 3705310..680ea1d 100644
--- a/src/lib/sample.ts
+++ b/src/lib/sample.ts
@@ -1,11 +1,8 @@
-export function foo() {
+export function foo(): void {
console.log('foo');
}
-export function bar() {
+export function bar(): void {
+ console.log('bar');
console.log('bar');
-}
-
-export function hoge() {
- console.log('hoge');
}
このまま git commit
を実行すると、このハンクをまるごと含む 1 つのコミットが出来上がります。より細かく分割したハンクをコミットしたいときは、git add --patch
の出番です。
--patch オプションの使用例
コミットを意味のある単位でまとめることで、コードレビューの効率が上がるといった様々なメリットがあります。しかし、よく気をつけていないとj同一ファイル内に複数の意図を持つ変更をしてしまいがちです。--patch
オプションの使い方を知っておくことで、この様なケースで手戻りなくコミットの粒度を調整できるようになります。
以下のような、ブログ記事のスラッグを表示する Next.js のページがあったとしましょう。
import { getSlugs } from "@/lib/articles";
type Params = {
slug: string;
};
type Props = {
params: Params;
};
export async function generateStaticParams() {
const slugs = await getSlugs();
return slugs.map((slug) => ({
slug,
}));
}
function Page({ params: { slug } }: Props) {
return <h1>{slug}</h1>;
}
export default Page;
開発中あれこれしているうちに、ファイル内に 3 つの意図を持つ変更が混在してしまいました。
generateStaticParams
の戻り値を明示的にする (リファクタリング)default export
をインラインで記述する (リファクタリング)- 見出し
<h1>
を大文字にする (機能変更)
@@ -8,7 +8,7 @@
params: Params;
};
-export async function generateStaticParams() {
+export async function generateStaticParams(): Promise<Params[]> {
const slugs = await getSlugs();
return slugs.map((slug) => ({
@@ -16,8 +16,6 @@
}));
}
-function Page({ params: { slug } }: Props) {
- return <h1>{slug}</h1>;
+export default function Page({ params: { slug } }: Props) {
+ return <h1 className="uppercase">{slug}</h1>;
}
-
-export default Page;
ハンクをリファクタリングと機能変更に分割して、2 つのコミットをつくります。コマンドを実行すると現在のハンクが表示され、実行する操作を尋ねられました。
git add --patch src/app/articles/\[slug\]/page.tsx
diff --git a/src/app/articles/[slug]/page.tsx b/src/app/articles/[slug]/page.tsx
index 43e36a3..5f41231 100644
--- a/src/app/articles/[slug]/page.tsx
+++ b/src/app/articles/[slug]/page.tsx
@@ -8,7 +8,7 @@ type Props = {
params: Params;
};
-export async function generateStaticParams() {
+export async function generateStaticParams(): Promise<Params[]> {
const slugs = await getSlugs();
return slugs.map((slug) => ({
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?
Git は変更を 2 つのハンクとして認識しているようです。?
を入力して Enter キーを叩くとヘルプが表示され、コマンドの意味を確認できます。リファクタリングの方からコミットしたいので、y
を選択してこのハンクをステージングします。
y - stage this hunk
n - do not stage this hunk
q - quit; do not stage this hunk or any of the remaining ones
a - stage this hunk and all later hunks in the file
d - do not stage this hunk or any of the later hunks in the file
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
g - select a hunk to go to
/ - search for a hunk matching the given regex
e - manually edit the current hunk
? - print help
@@ -8,7 +8,7 @@ type Props = {
params: Params;
};
-export async function generateStaticParams() {
+export async function generateStaticParams(): Promise<Params[]> {
const slugs = await getSlugs();
return slugs.map((slug) => ({
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
2 つ目のハンクはリファクタリングと機能変更の両方を含んでいます。リファクタリングの分だけステージングしたいので、ハンクをマニュアルで編集する e
を選択します。
@@ -16,8 +16,6 @@ export async function generateStaticParams() {
}));
}
-function Page({ params: { slug } }: Props) {
- return <h1>{slug}</h1>;
+export default function Page({ params: { slug } }: Props) {
+ return <h1 className="uppercase">{slug}</h1>;
}
-
-export default Page;
(2/2) Stage this hunk [y,n,q,a,d,K,g,/,s,e,?]? e
Vim が起動して、ハンクのマニュアル編集モードに入りました。
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -16,8 +16,6 @@ export async function generateStaticParams() {
}));
}
-function Page({ params: { slug } }: Props) {
- return <h1>{slug}</h1>;
+export default function Page({ params: { slug } }: Props) {
+ return <h1 className="uppercase">{slug}</h1>;
}
-
-export default Page;
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
# If the patch applies cleanly, the edited hunk will immediately be marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again. If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.
よく見るとハンク内の各行は 3 種類の記号から始まっていることがわかります。それぞれの意味は次の通りで、プラスまたはマイナスから始まる行がステージング対象となります。
+
から始まる行: 追加-
から始まる行: 削除
コメントとして表示されているガイドに従い、ステージング対象から外したい行を編集します。
+
から始まる行は行自体を削除する-
から始まる行はマイナスを半角スペースに置換する- 行の並び順を調整する
# Manual hunk edit mode -- see bottom for a quick guide.
@@ -16,8 +16,6 @@ export async function generateStaticParams() {
}));
}
-function Page({ params: { slug } }: Props) {
+export default function Page({ params: { slug } }: Props) {
return <h1>{slug}</h1>;
}
-
-export default Page;
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
# If the patch applies cleanly, the edited hunk will immediately be marked for staging.
# If it does not apply cleanly, you will be given an opportunity to
# edit again. If all lines of the hunk are removed, then the edit is
# aborted and the hunk is left unchanged.
保存して Vim を閉じると、変更内容に問題がなければコマンドの対話モードが終了します。意図した部分 (リファクタリング) のみステージングされていることを確認しましょう。
git diff --staged
diff --git a/src/app/articles/[slug]/page.tsx b/src/app/articles/[slug]/page.tsx
index 43e36a3..274a911 100644
--- a/src/app/articles/[slug]/page.tsx
+++ b/src/app/articles/[slug]/page.tsx
@@ -8,7 +8,7 @@ type Props = {
params: Params;
};
-export async function generateStaticParams() {
+export async function generateStaticParams(): Promise<Params[]> {
const slugs = await getSlugs();
return slugs.map((slug) => ({
@@ -16,8 +16,6 @@ export async function generateStaticParams() {
}));
}
-function Page({ params: { slug } }: Props) {
+export default function Page({ params: { slug } }: Props) {
return <h1>{slug}</h1>;
}
-
-export default Page;
問題がなかったので、1 つ目のコミットをつくります。
git commit -m "refactor: スタイルガイドから外れているコードを修正する"
直前のコミットでリファクタリングに関する変更を抽出したので、機能変更の部分のみが残っているはずです。
git diff
diff --git a/src/app/articles/[slug]/page.tsx b/src/app/articles/[slug]/page.tsx
index 274a911..5f41231 100644
--- a/src/app/articles/[slug]/page.tsx
+++ b/src/app/articles/[slug]/page.tsx
@@ -17,5 +17,5 @@ export async function generateStaticParams(): Promise<Params[]> {
}
export default function Page({ params: { slug } }: Props) {
- return <h1>{slug}</h1>;
+ return <h1 className="uppercase">{slug}</h1>;
}
問題がなかったので、2 つ目のコミットをつくります。
git commit -am "feat: 記事内の表現に強弱をつけるため、タイトルを大文字にする"
ログを確認すると・・・
git log --pretty=oneline
cf76bbfe08d21ed8678ef1b27e57ac6934e31148 (HEAD -> main) feat: 記事内の表現に強弱をつけるため、タイトルを大文字にする
d83dcc1fe9f0b3d73fc3c26a28b0ccfa677dc90c refactor: スタイルガイドから外れているコードを修正する
コミットを分割できました!
おわりに
git add の --patch
オプションを紹介しました。普段から使うようなコマンドではありませんが、コミットの内容を几帳面に調整するときに必要な場面が出てきます。今後はマニュアル編集モードをうまく活用して、まとまりの良いコミットをつくる様に意識していきたいです。
参考
育ちのよいコード | プログラマが知るべき97のことxn--97-273ae6a4irb6e2hsoiozc2g4b8082p.com