ymmr
Recent Articles
    公開日: 2023/10/04

    git add --patch でまとまりの良いコミットをつくる

    git add の --patch オプションについて、実例を交えて用途や使用方法を紹介します。関連のある変更を 1 つのコミットにまとめ、コードベースを "育ちのよいコード" にしていくうえで何かの参考になれば幸いです。

    --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 のページがあったとしましょう。

    page.tsx
    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 種類の記号から始まっていることがわかります。それぞれの意味は次の通りで、プラスまたはマイナスから始まる行がステージング対象となります。

    • + から始まる行: 追加
    • - から始まる行: 削除
    • (半角スペース) から始まる行: 変更なし

    コメントとして表示されているガイドに従い、ステージング対象から外したい行を編集します。

    1. + から始まる行は行自体を削除する
    2. - から始まる行はマイナスを半角スペースに置換する
    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