🧹

Git rebaseでコミット整理とブランチ管理を改善

に公開

はじめに

この記事について

この記事では、Git rebaseを使ったコミット履歴の整理方法と、ブランチの分岐元を変更する運用について解説します。具体的には以下の内容を学ぶことができます:

  • コミットの整理方法:fixup、squash、drop、editコマンドの使い分けと実践的な使用方法
  • コミットメッセージの変更:直前のコミットから過去の任意のコミットまでの修正方法
  • ブランチ運用の改善:マージではなくrebaseを使った分岐元の変更によるメリット
  • 安全なforce push--force-with-lease --force-if-includesを使った安全な履歴の更新方法

対象読者

  • Gitの基本的な操作(add、commit、push、pull)を理解している方
  • チーム開発でGitを使用しており、コードレビューを行っている方
  • コミット履歴を整理してMR(マージリクエスト)をきれいに保ちたい方
  • Git rebaseを使ったことがあるが、体系的に理解したい方

想定環境

  • Git 2.30以降(--force-if-includesオプションを使用するため)
  • GitLab、GitHub、Bitbucketなどのリモートリポジトリを使用

1. Git rebaseでコミットをまとめる方法

基本的なrebaseの流れ

# インタラクティブモードでrebaseを開始
git rebase -i HEAD~3  # 直近3つのコミットを対象にする

エディタが開き、以下のような画面が表示されます:

pick abc1234 feat: 新機能の追加
pick def5678 fix: バグ修正
pick ghi9012 docs: ドキュメント更新

# Rebase abc1234..ghi9012 onto abc1234 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#

pick となっている部分を、fixupや、squasheditなどに書き換えてコミットログを操作します。
それぞれの説明は以下にまとめます。

2. rebaseコマンドの詳細解説

2.1 fixup - コミットメッセージを破棄して前のコミットに統合する

fixup前の状態:


fixup後の状態:

JetBrains IDEを使っている場合は、gitのパネルを表示し、rebaseの起点にしたいコミットを右クリックして、ここから対話的にリベースを選び、リベースの画面でフィックスアップを選択することでfixupできます

使用場面:

  • コミットメッセージを編集せず、コミットをまとめるとき

2.2 squash - コミットメッセージを保持して統合

squash前の状態:

squash後の状態:

JetBrains IDEを使っている場合は、gitのパネルを表示し、rebaseの起点にしたいコミットを右クリックして、ここから対話的にリベースを選び、リベースの画面でスカッシュを選択することでsquashできます

使用場面:

  • コミットメッセージを編集して、コミットをまとめるとき

2.3 drop - コミットを削除

drop前の状態:

drop後の状態:

JetBrains IDEを使っている場合は、gitのパネルを表示し、rebaseの起点にしたいコミットを右クリックして、ここから対話的にリベースを選び、リベースの画面で破棄を選択することでdropできます

使用場面:

  • デバッグ用のコミットを削除
  • 不要な実験的変更を取り除く
  • 間違ったコミットを除外

2.4 edit - コミットを分割・修正

editコマンドは、コミットの内容を変更したり、1つのコミットを複数に分割したりする際に使用します。

editの詳細な手順

# 1. rebaseを開始
git rebase -i HEAD~2

# 2. エディタで対象のコミットをeditに変更
pick abc1234 feat: 複数の機能を一度に追加
edit def5678 feat: ユーザー管理機能(認証・プロフィール・設定)  # ← このコミットを分割
pick ghi9012 docs: READMEを更新

# 3. rebaseが一時停止したら、コミットをリセット
git reset HEAD^

# 4. ファイルを個別にaddしてコミット
git add auth.js
git commit -m "feat: ユーザー認証機能を追加"

git add profile.js
git commit -m "feat: ユーザープロフィール機能を追加"

git add settings.js
git commit -m "feat: ユーザー設定機能を追加"

# 5. rebaseを続行
git rebase --continue

edit前の状態(1つの大きなコミット):

edit後の状態(分割されたコミット):

editの使用場面

  1. 大きなコミットを論理的に分割

    • 複数の機能が1つのコミットに含まれている
    • レビューしやすい単位に分けたい
  2. 機密情報の削除

    # editで停止後
    git reset HEAD^
    # 機密情報を含むファイルを修正
    vim config.js  # パスワードを環境変数に変更
    git add .
    git commit -m "feat: 設定ファイルを追加(環境変数を使用)"
    
  3. ファイルの追加・削除

    # editで停止後
    # 不要なファイルを削除
    git rm debug.log
    # 必要なファイルを追加
    git add .gitignore
    git commit --amend
    

editコマンドの注意点

  • git reset HEAD^でコミットを取り消すが、作業ディレクトリの変更は保持される
  • 分割する際は、関連する変更をまとめて論理的な単位でコミットする
  • git rebase --continueを忘れずに実行する
  • 途中で作業を中止したい場合はgit rebase --abortを使用

3. コミットメッセージの変更方法

3.1 rewordを使った変更

rewordは、コミットメッセージのみを変更したい場合に最も簡単な方法です。

# rebase時に
pick abc1234 feat: 新機能
reword def5678 fix: バグ修正  # pickをrewordに変更
pick ghi9012 docs: ドキュメント更新

# 保存して閉じると、該当コミットのメッセージ編集画面が開く

3.2 amendを使った直前のコミット修正

直前のコミットメッセージを変更する場合:

# メッセージのみ変更
git commit --amend -m "feat: 改善された新機能"

# または、エディタで編集
git commit --amend

ファイルの変更も含める場合:

# ファイルを修正後
git add .
git commit --amend -m "feat: 改善された新機能"

3.3 n個前のコミットメッセージを変更(rebase + edit + amend)

過去の特定のコミットメッセージを変更する場合の手順:

# 1. 例:3個前のコミットメッセージを変更したい場合
git log --oneline -5
# 出力例:
# abc1234 (HEAD -> feature) feat: 最新の機能
# def5678 fix: バグ修正
# ghi9012 feat: タイプミスがある機能  # ← これを修正したい
# jkl3456 docs: README更新
# mno7890 Initial commit

# 2. インタラクティブrebaseを開始(修正したいコミットが含まれる範囲を指定)
git rebase -i HEAD~4

# 3. エディタで、修正したいコミットをeditに変更
pick jkl3456 docs: README更新
edit ghi9012 feat: タイプミスがある機能  # pickをeditに変更
pick def5678 fix: バグ修正
pick abc1234 feat: 最新の機能

# 4. 保存して閉じると、該当のコミットで停止
# Stopped at ghi9012... feat: タイプミスがある機能

# 5. コミットメッセージを修正
git commit --amend -m "feat: ユーザー認証機能"

# 6. rebaseを続行
git rebase --continue

3.4 JetBrains IDEを使ったメッセージ変更方法

gitのパネルを表示し、rebaseの起点にしたいコミットを右クリックして、コミットメッセージの編集を選択することでメッセージ変更できます

4. ブランチの分岐元を変更する運用

現在の問題点:メインブランチをマージする運用

マージによる運用の状態:

現在、私の所属する部では、main に取り込まれた修正が、作業ブランチでも欲しい場合に、現在はmainブランチをマージして修正を取り込む運用になっています。
この場合、以下のようなデメリットがあります。

デメリット

  1. レビューの複雑化

    • GitLabのMRに他の開発者の変更が混入
    • 実際の変更内容が把握しづらい
  2. 履歴の複雑化

    • マージコミットによる履歴の分岐
    • git logが読みづらくなる
    • マージコミットがあると rebase によりコミットをまとめることができなくなる
      (マージコミット以降のコミットしかまとめられない)
  3. コンフリクトリスクの増大

    • マージ時のコンフリクト解決が必要
    • 同じコンフリクトを複数回解決する可能性(マージで取り込んだファイルを複数ブランチで修正した場合)

解決策:rebaseによる分岐元の変更

分岐元を変更するコマンド

main の修正を取り込む場合は以下のようなコマンドを実行することで、現在の作業ブランチが main の最新のコミットから分岐したような構造に変更することが可能です。

# featureブランチで作業中に、mainブランチの最新を取り込む
git checkout main
git pull origin main

# featureブランチに移動
git checkout feature/xxxx

# mainブランチの最新コミットを基点にrebase
git rebase main

rebase前の状態:

rebase後の状態:

JetBrains IDEを使っている場合は、gitのパネルを表示し、新しい分岐元にするコミットを右クリックして、ブランチ <分岐元ブランチ名>を選び、<分岐元ブランチ名> で <作業ブランチ名> をリベース を選択することで、分岐元を変更できます。

rebaseのメリット

  1. クリーンな履歴

    • 直線的なコミット履歴
    • レビューが容易
  2. 明確な変更内容

    • MRには自分の変更のみが表示
    • 変更の影響範囲が明確
  3. 効率的な開発

    • マージコミットが不要
    • コンフリクト解決が1回で済む

5. Git pushとforce pushの安全な使い方

5.1 rebase後のpushについて

rebaseを行うと、コミットのハッシュ値が変更されるため、通常のpushではエラーが発生します。

# rebase後に通常のpushを試みた場合
$ git push origin feature
To https://github.com/example/repo.git
 ! [rejected]        feature -> feature (non-fast-forward)
error: failed to push some refs to 'https://github.com/example/repo.git'

5.2 force pushのリスク

単純な--forceオプションは危険です:

# 危険な例(使用を避けるべき)
git push --force origin feature

リスク:

  • 他の開発者の変更を無条件で上書き
  • チームメンバーの作業が失われる可能性
  • リモートの最新状態を確認せずに上書き

5.3 安全なforce push:--force-with-lease

--force-with-leaseは、リモートブランチが期待する状態の場合のみpushします:

# より安全な方法
git push --force-with-lease origin feature

動作:

  • ローカルが把握しているリモートの状態と、実際のリモートの状態を比較
  • 誰かが先にpushしていた場合は拒否される

5.4 最も安全な方法:--force-with-lease --force-if-includes

Git 2.30以降で利用可能な、最も安全なオプションの組み合わせ:

# 推奨:最も安全な方法
git push --force-with-lease --force-if-includes origin feature

追加の保護:

  • --force-if-includesは、リモートブランチのコミットがローカルのreflogに含まれていることを確認
  • fetchした後に他の人がpushした変更を誤って上書きすることを防ぐ

5.5 実践的な運用フロー

# 1. mainブランチを最新に更新
git checkout main
git pull origin main

# 2. 作業ブランチをrebase
git checkout feature
git rebase main

# 3. コミットを整理
git rebase -i main

# 4. リモートの最新状態を確認
git fetch origin

# 5. 安全にforce push
git push --force-with-lease --force-if-includes origin feature

5.6 pushのエイリアス設定

以下のエイリアスを設定すると pushfpush --force-with-lease --force-if-includes と同様の動作ができるようになります。

# ~/.gitconfigに追加
[alias]
   pushf = push --force-with-lease --force-if-includes

# 使用例
git pushf origin feature

最後に

Git rebaseは強力なツールですが、その分慎重に扱う必要があります。最初は簡単な操作から始めて、徐々に複雑な操作に挑戦していくことをお勧めします。

この記事で紹介した手法を実践することで、より読みやすく、メンテナンスしやすいGit履歴を維持できるようになります。チーム全体でこれらの運用を導入することで、開発効率とコード品質の向上が期待できるでしょう。

https://zenn.dev/owayo/articles/a009820118060f

https://zenn.dev/mary_pp/articles/eaac544eaf600a

Discussion