はじめに
こんにちは!k-furusawaです。
いきなりですが、みなさん、コミットしてますか?
コミットって言葉が広義すぎて戸惑いますね。ここではGitのコミットのことです。
Gitコミットはついつい細かくしすぎたり大きくしすぎたりしますよね。
ぶっちゃけちゃんとルールを設けて運用している人の方が少ないんじゃないでしょうか。
でも本当にそれで大丈夫?今日はそんなGitコミットについて考えていきたいと思います!
この記事の対象者
- 最近Gitを使い出してなんとなく慣れてきた人
- Gitコミットが乱れがちな人
- よくプルリク出して「長い」と言われがちな人
- コミットに何を書けばいいのか正直雰囲気で決めている人
特に、チーム開発・レビュー前提のプロジェクトを想定しています。
目次
本当にテキトーでいいのか?! Gitコミット履歴をよく観察する
たとえば以下のようなコミットをよく見かけると思います。
コミットA「機能A追加」
コミットB「コミットAの修正」
コミットC「コミットBのrevert」
コミットD「コミットAの修正」
コミットE「機能Bの変更」
このコミット履歴、実のところ以下しかないです。
コミットA「機能A追加」
コミットE「機能Bの変更」
B,C,Dが履歴としては完全に無駄なんですね。
あるいはコミットAがすでにデプロイ済みだった場合でも
コミットA「機能A追加」
コミットB「コミットAの修正」
コミットE「機能Bの変更」
これだけでいいです。つまり以下のようになってるわけです。
コミットA「機能追加」
コミットB「コミットAの修正」 <- デプロイ前なら意味がない
コミットC「コミットBのrevert」 <- コミットAに戻っただけで意味がない
コミットD「コミットAの修正」 <- デプロイ前なら意味がない。デプロイ後ならこれがメインになる
コミットE「機能Bの変更」
このように往々にしてGitコミットには無駄が多かったりします。
Gitコミットだって立派な資産である
Gitコミットくらい汚れててもいいや、みたいな軽い感覚でいたりしないでしょうか?
とんでもない!Gitコミットも立派な資産です。自社開発ならば自社の、委託であえば委託先の、常駐であればお客様の資産です。
個人プロジェクトなら汚れていても文句は言われませんが、会社として管理されているものであるからには資産である認識を持った方がいいです。
改善方法を考えてみる
コミットは1つ1つが意味を持たなければただのゴミになるので、コミット単位をよく考える必要があります。
個人的には以下のことを意識しながらコミットしています。
- コミットコメントに意味のないものを書かない
- prefixも意識する
- 自分用ならそもそもプッシュしない(場合によってはスタッシュでいい)
- ブランチを最大限に活用する
コミットコメントに意味のないものを書かない
先ほどの例をもう一度
コミットA「機能A追加」
コミットB「コミットAの修正」
コミットC「コミットBのrevert」
コミットD「コミットAの修正」
コミットE「機能Bの変更」
これ、読んだ時に分かる人はどんな人でしょうか?
開発者自身ですね。それ以外の人には何か分かりません。何か事情があって何かしたのはなんとなく察しますが、何を目的にしたコミットなのかは中身を見て詳細を読む必要があります。
ではこうなっていたらどうでしょう?
コミットA「機能Aを追加」
コミットB「メソッドAの変換バグ修正」
コミットC「メソッドAの変換バグ修正の取り消し」
コミットD「メソッドAの変換バグ修正」
コミットE「機能Bの変更」
コミットのコメントだけで何となく経緯がわかるかと思います。
このように、意味のない文字列を入れて満足するのは避けるのが無難です。
prefixも意識する
コミットコメントにprefixルールを設けるとなお良くなります。
prefixルールというのは、修正ならば fix 、変更なら change 、といったものです。具体的には以下のようなものです。
コミットA「(feat)機能Aを追加」
コミットB「(fix)メソッドAの変換バグ修正」
コミットC「(revert)メソッドAの変換バグ修正の取り消し」
コミットD「(fix)メソッドAの変換バグ修正」
コミットE「(change)機能Bの変更」
prefixルールはいろんな記事が出ているので検索してみてください。
prefixの種類をどこまで細かくするのか、どれにどれをつけるのかはプロジェクトごとに違うので、これ!という正解はありません。
プロジェクト内でよく話し合って決めると良いと思います。
自分用ならそもそもプッシュしない
コミット内に、たまにメモ書きみたいなのが混ざっているのをよく見かけます。
コミットA「(feat)機能Aを追加」
コミットB「(fix)メソッドAの変換バグ修正」
コミットC「(revert)メソッドAの変換バグ修正の取り消し」
コミットD「ローカルのバグを回避する」
コミットE「(fix)メソッドAの変換バグ修正」
コミットF「(change)機能Bの変更」
コミットDです。
これは極端な例ですが、ローカルでの事情をコミット内に持ち込んでいますね。それがチーム全体に発生するなら構築の問題なのでそれ用の修正をコミットします。
個人の環境の事情を持ち込む場所ではありません。なんであればスタッシュして個人で管理した方がいいです。
ブランチを最大限に活用する
今までコミット履歴を1つの流れで書きましたが、Gitにはブランチがあるのでこれも最大限に活用したいところです。
たとえば以下のコミット履歴ですが、1つのブランチで管理するべきではありません。
コミットA「(feat)機能Aを追加」 <- デプロイ済みと仮定
コミットB「(fix)メソッドAの変換バグ修正」
コミットC「(change)機能Bの変更」
コミットAがデプロイ済みだとしたら、コミットBとCは依存しないのでまるで別の要件です。
このコミットは以下のようになっているべきだと考えられます。
mainブランチ
コミットA「(feat)機能Aを追加」 <- デプロイ済みと仮定
修正ブランチ
コミットA「(fix)メソッドAの変換バグ修正」
機能追加ブランチ
コミットA「(change)機能Bの変更」
それぞれが分離しており、mainブランチへのPR作成も完全に分けてレビュー可能になります。
目的単位で意味のあるコミットにまとめる
上記までの内容を踏まえて、コミットを改善した例を示します。
例: 忌避すべき状態
コミットA「テーブル追加」
コミットB「機能追加」
コミットC「コミットAの修正」 <- デプロイ前なら意味がない
コミットD「コミットBのrevert」 <- コミットAに戻っただけで意味がない
コミットE「コミットAの修正」 <- デプロイ前なら意味がない。デプロイ後ならこれがメインになる
コミットF「APIの追加」
例: コミットを整理してprefixを設けた状態
コミットA「(feat)テーブルAの追加とモデルの追加」
コミットB「(feat)機能Aの追加」
コミットC「(fix)機能Aの⚪︎⚪︎バグの修正」 <- コミットAのデプロイ前ならこれもコミットBにまとめる
コミットD「(change)機能Bの⚪︎⚪︎を⚪︎⚪︎に変更」
コミットE「(feat)⚪︎⚪︎APIの追加」
さらに、この整理の最中にAPIのために追加した新メソッドと、既存メソッドに対する変更があることに気づいたとします。
コミットA「(feat)テーブルAに追加とモデルの追加」
コミットB「(feat)メソッドAの追加」
コミットC「(change)メソッドBの変更」 <- 既存の別機能に影響を与える変更
コミットD「(feat)⚪︎⚪︎APIの追加」
といった具合に分けることが肝要です。
コミットが分かれているのでプルリクエストも分離できる
コミットが分かれていることで、mainブランチにマージされる内容も分離されます。
プルリクA
コミットA「(feat)テーブルAに追加とモデルの追加」
プルリクB
コミットB「(feat)メソッドAの追加」
プルリクC
コミットC「(change)メソッドBの変更」
プルリクD
コミットD「(feat)⚪︎⚪︎APIの追加」
極端な例ではありますが、プルリクもまた1つの目的に集約されていると良いです。
「理想だけど、そんなに分けてられない」
「小さい修正までPR分けるのは現実的じゃない」
もちろんです!すべての変更をここまで厳密に分ける必要はありません。
重要なのは「あとから安全に戻せる単位になっているか」「意味を持たない変更が混ざっていないか」です。
- 障害時に切り戻せるか
- レビュー時に影響範囲を即座に把握できるか
この2点を満たしていれば、粒度はプロジェクトに合わせて調整して問題ありません。
問題発生時の切り戻しが安全かつ楽
コミット1つにつき1つの意味を持つので障害時の発見と変更すべき点の絞り込みに時間がかからなくなります。パージも楽。
コミットA「(feat)テーブルAに追加とモデルの追加」 <- dev環境に反映。問題なし
コミットB「(feat)メソッドAの追加」 <- dev環境の反映。問題なし
コミットC「(change)メソッドBの変更」 <- dev環境に反映。バグが発生。バグ発生箇所が明確。コミットBまで戻す。
コミットD「(feat)新APIの追加」 <- コミットCが直るまでデプロイを見合わせ
マージする順番を間違えそう?
それは運用次第かと思います。GitHubではDraft PRを作成できるので、1番目のPRがマージされるまではPRをOpenにしない、などの運用を行い、依存するPR同士に #12 のようにマークすることで漏れも防げます。
コミットを整理することで得られるメリット
そんな細かくしたくないよ!という人は当然いるかと思います。
しかしコミットを整理すると得られるものがいろいろあります。
自分が何を目的としているのかの再把握につながる
コミットが乱れている時は迷走していることが多いので、自分が今現在何を目的としているのかの整理でもあります。
コミットの整理を行うと強制的に自己レビューすることになる
コミットに含まれている内容を強制的に自己レビューすることになるので、漏れ、過剰な修正、変更点の絞り込みができます。
課題管理と密結合できる
課題をIssueで管理している場合、コミットにIssue番号を含めるとリンクされます(GitHubの場合)。これによって、このコミット変更が後からどの課題のものだったのかすぐにわかるようになります。更に、課題内容から逸脱した変更が含まれることで、思わぬ不具合が発生することを防げます。
レビューが超楽になる(重要)
プルリクエストを作成する際も、意味のあるコミット単位になります。
するとどうでしょう!
レビューが超高速になります。なにせ意味のないものが混ざってないので、どれがどこまで影響するのか、といった精査が短時間で済むようになります。たとえばテーブルの追加(マイグレーション)と新メソッドの追加は既存に影響しませんよね(マイグレーションがするかもですが)。既存への影響があるPRに集中して時間をかけることができます。
問題発生時のリカバリが楽になる
これは前項でも書きましたが、それぞれのコミットが1つの意味を持っているので、不具合発生時の発見が早くなります。コミット内にはその範囲のものしか含まれていないので、そのコミットをそのままrevertするなりdropするなり、何であればrebaseすれば問題箇所を取り除けます。
終わりに
いかがだったでしょうか。
単にコミットを整理しろ!だと、何をどうするべきなのかってなかなか難しいですよね。
そんな時の一助になるような記事になれていれば幸いです。
では、また何か書くことがあればお会いしましょう!
付録
コミット整理したいけどコマンドがわからん!って人もいるかもなので、よく使うコマンドをご紹介。詳しい使い方は個人で調べていただいて。
- git rebase
複数のコミットをまとめることができます。最もよく使うかと思います。
まとめたいものをスカッシュ。省きたいものをドロップ。みたいな感じです。
うまく使うと真ん中に挟まったコミットに対して修正コミットを打つこともできます。 - git cherry-pick
別のブランチから一部だけを取り込みたい時に使うやつです。
個人的にはあまり使わないのですが、変更の初号、fix版、など複数で管理する人は割と使うと思います。 - git reset HEAD^ --soft
あー!コミットミスった!となったら取り消すやつです。面倒くさくなったらこれで最初まで戻ってから整理し直すとかも良くやります。
hardすると変更も消えるので気をつけてください。