blog

ソフトウェア開発|Git varbasesで履歴を変更する!

Gitの中心にある付加価値の一つは、履歴を編集できることです。履歴を神聖な記録として扱うバージョン管理システムとは異なり、Gitでは履歴を必要に応じて変更することができます。...

Oct 23, 2025 · 18 min. read
シェア

中心にある付加価値の一つは、履歴を編集できることです。履歴を神聖な記録として扱うバージョン管理システムとは異なり、Gitでは必要に応じて履歴を修正することが可能です。これは、リファクタリングが良いソフトウェア設計のプラクティスを維持するために使われるのと同じように、良いコミット履歴を編むための強力なツールの数々を提供します。これらのツールは、新しい Git ユーザーや経験豊富な Git ユーザーにとっても少々とっつきにくいものですが、このガイドでは強力な git-rebase の使い方をわかりやすく説明します。

注意すべき点: 一般に、公開ブランチや共有ブランチ、安定版ブランチの履歴は編集しないことを推奨します。機能ブランチや個人ブランチ、そしてまだプッシュされていないコミットの履歴を編集することは可能です。コミットを編集した後で git push -f を使用すると、個人ブランチやトピックブランチに強制的にプッシュすることができます。

この悲惨な警告にもかかわらず、このガイドで述べられていることはすべて非破壊的な操作であることを述べておく価値があります。実際、Gitでデータを永久に失うことは非常に困難です。このガイドの最後には、もし間違いを犯してしまった場合の修正方法が書かれています。

サンドボックスの設定

実際のリポジトリを壊したくないので、このガイドではサンドボックス化されたリポジトリを使用します。以下のコマンドを実行して開始してください。

  1. git init /tmp/rebase-sandbox
  2. cd /tmp/rebase-sandbox
  3. git commit --allow-empty -m"Initial commit"

問題が発生した場合は、 rm -rf /tmp/rebase-sandbox実行し、これらのステップを再実行してやり直すだけです。このガイドの各ステップは新しいサンドボックスで実行できるので、すべてのタスクをやり直す必要はありません。

最近のコミットの修正

まずは簡単なことから始めましょう。直近のコミットを修正するのです。サンドボックスにファイルを追加してミスを犯してみましょう。

  1. echo "Hello wrold!" >greeting.txt
  2. git add greeting.txt
  3. git commit -m"Add greeting.txt"

このバグの修正はとても簡単です。次のようにファイルを編集して--amendをつけて投稿するだけです:

  1. echo "Hello world!" >greeting.txt
  2. git commit -a --amend

a を指定すると、Git がすでに知っているすべてのファイルを自動的にステージします。また、--amend を指定すると、変更を最新のコミットにまとめます。保存してエディタを終了します。修復されたコミットは、git show を実行すれば見ることができます。

  1. commit f5f19fbf6d35b2db37dcac3a55289ff (HEAD -> master)
  2. Author: Drew DeVault
  3. Date: Sun Apr : -0400
  4. Add greeting.txt
  5. diff --git a/greeting.txt b/greeting.txt
  6. new file mode
  7. index ..cd78055
  8. --- /dev/null
  9. +++ b/greeting.txt
  10. @@ -0,0 +1 @@
  11. +Hello world!

古いコミットの修正

--amend は最近のコミットにのみ適用されます。古いコミットを修正する必要がある場合はどうなるでしょうか?まずはサンドボックスの設定から始めましょう:

  1. echo "Hello!" >greeting.txt
  2. git add greeting.txt
  3. git commit -m"Add greeting.txt"
  4. echo "Goodbye world!" >farewell.txt
  5. git add farewell.txt
  6. git commit -m"Add farewell.txt"

greeting.txt "world "が抜けているようです。これを修正するために普通にコミットを書きましょう:

  1. echo "Hello world!" >greeting.txt
  2. git commit -a -m"fixup greeting.txt"

これでファイルは正しく見えますが、履歴はもう少しよくなります。最後のコミットを新しいコミットで「修正」することができます。これを行うには、対話的リベースという新しいツールを導入する必要があります。最後の 3 つのコミットをこの方法で編集するので、git rebase -i HEAD~3 を実行します:

  1. pick 8d3fc77 Add greeting.txt
  2. pick 2a73a77 Add farewell.txt
  3. pick 0b9d0bb fixup greeting.txt
  4. # Rebase f5f19fb..0b9d0bb onto f5f19fb (3 commands)
  5. # Commands:
  6. # p, pick <commit> = use commit
  7. # f, fixup <commit> = like "squash", but discard this commit's log message

このファイルを編集することで、Git に履歴の編集方法を指示することができます。この要約は、基本変更計画のこの部分に関連する詳細な内容だけに削りましたが、テキストエディタで完全な要約を見ることができます。

保存してエディターを閉じると、Git はそれらのコミットをすべて履歴から削除し、一行ずつ実行します。デフォルトでは、それぞれのコミットをヒープから取り出してブランチに追加します。このファイルにまったく編集が加えられていない場合は、最初に戻ってそのまま各コミットを実行します。さて、いよいよ私のお気に入りの機能のひとつである repair を使ってみましょう。三行目を編集してアクションを pick から fixup に変更し、「修正」したいコミットの直後に移動します:

  1. pick 8d3fc77 Add greeting.txt
  2. fixup 0b9d0bb fixup greeting.txt
  3. pick 2a73a77 Add farewell.txt

ヒント:次回のスピードを上げるために、fと省略することもできます。

保存してエディターを終了すると、Gitがこれらのコマンドを実行します。ログを見て結果を確認することができます:

  1. $ git log -2 --oneline
  2. fcff6ae (HEAD -> master) Add farewell.txt
  3. a479e94 Add greeting.txt

複数の投稿を1つにフラット化

小さなマイルストーンに取り組むときや前のコミットのバグを修正するときには、たくさんのコミットを書くと便利です。しかし、master ブランチに作業をマージする際には、これらのコミットを "スクシャッシュ" して履歴をわかりやすくしておくと便利です。これを行うには、"squash" 操作を使用します。まずはたくさんのコミットを書いてみましょう。スピードアップしたい場合は、それをコピーして貼り付けるだけです:

  1. git checkout -b squash
  2. for c in H e l l o , ' ' w o r l d; do
  3. echo "$c" >>squash.txt
  4. git add squash.txt
  5. git commit -m"Add '$c' to squash.txt"

"こんにちは、世界 "というファイルを作るには、いろいろなことが必要です。ベースの別のインタラクティブなバリエーションを開始し、それらを一緒につぶしてみましょう。これを試すために、最初にブランチがチェックアウトされたことに注意しましょう。git rebase -i master でブランチが作成されたので、すべてのコミットをすばやくリベースすることができます。その結果

  1. pick 1e85199 Add 'H' to squash.txt
  2. pick fff6631 Add 'e' to squash.txt
  3. pick b354c74 Add 'l' to squash.txt
  4. pick 04aaf74 Add 'l' to squash.txt
  5. pick 9b0f720 Add 'o' to squash.txt
  6. pick d Add ',' to squash.txt
  7. pick dc158cd Add ' ' to squash.txt
  8. pick dfcf9d6 Add 'w' to squash.txt
  9. pick 7a85f34 Add 'o' to squash.txt
  10. pick c275c27 Add 'r' to squash.txt
  11. pick a513fd1 Add 'l' to squash.txt
  12. pick 6b608ae Add 'd' to squash.txt
  13. # Rebase 1af1b46..6b608ae onto 1af1b46 (12 commands)
  14. # Commands:
  15. # p, pick <commit> = use commit
  16. # s, squash <commit> = use commit, but meld into previous commit

ヒント: ローカルの master ブランチはリモートの master ブランチとは独立して進化し、Git はリモートブランチを origin/master として保存します。このテクニックと組み合わせることで、上流にマージされていないコミットをリベースするのにとても便利な方法とgit rebase -i origin/master

を実行すると、これらの変更がすべて最初のコミットに取り込まれます。これを行うには、以下のように最初の行以外のすべての "pick" アクションを "squash" に変更します:

  1. pick 1e85199 Add 'H' to squash.txt
  2. squash fff6631 Add 'e' to squash.txt
  3. squash b354c74 Add 'l' to squash.txt
  4. squash 04aaf74 Add 'l' to squash.txt
  5. squash 9b0f720 Add 'o' to squash.txt
  6. squash d Add ',' to squash.txt
  7. squash dc158cd Add ' ' to squash.txt
  8. squash dfcf9d6 Add 'w' to squash.txt
  9. squash 7a85f34 Add 'o' to squash.txt
  10. squash c275c27 Add 'r' to squash.txt
  11. squash a513fd1 Add 'l' to squash.txt
  12. squash 6b608ae Add 'd' to squash.txt

保存してエディターを閉じると、Git はしばらく考えてから再びエディターを開き、最終的なコミットメッセージを修正します。次のように表示されます:

  1. # This is a combination of 12 commits.
  2. # This is the 1st commit message:
  3. Add 'H' to squash.txt
  4. # This is the commit message #2:
  5. Add 'e' to squash.txt
  6. # This is the commit message #3:
  7. Add 'l' to squash.txt
  8. # This is the commit message #4:
  9. Add 'l' to squash.txt
  10. # This is the commit message #5:
  11. Add 'o' to squash.txt
  12. # This is the commit message #6:
  13. Add ',' to squash.txt
  14. # This is the commit message #7:
  15. Add ' ' to squash.txt
  16. # This is the commit message #8:
  17. Add 'w' to squash.txt
  18. # This is the commit message #9:
  19. Add 'o' to squash.txt
  20. # This is the commit message #10:
  21. Add 'r' to squash.txt
  22. # This is the commit message #11:
  23. Add 'l' to squash.txt
  24. # This is the commit message #12:
  25. Add 'd' to squash.txt
  26. # Please enter the commit message for your changes. Lines starting
  27. # with '#' will be ignored, and an empty message aborts the commit.
  28. # Date: Sun Apr : -0400
  29. # interactive rebase in progress; onto 1af1b46
  30. # Last commands done (12 commands done):
  31. # squash a513fd1 Add 'l' to squash.txt
  32. # squash 6b608ae Add 'd' to squash.txt
  33. # No commands remaining.
  34. # You are currently rebasing branch 'squash' on '1af1b46'.
  35. # Changes to be committed:
  36. # new file: squash.txt

ヒント:前のセクションで説明した "repair "コマンドもこの目的に使えますが、これはつぶれたコミットを破棄します。

すべてを削除して、以下のようなよりよいコミットメッセージに置き換えてみましょう:

  1. Add squash.txt with contents "Hello, world"
  2. # Please enter the commit message for your changes. Lines starting
  3. # with '#' will be ignored, and an empty message aborts the commit.
  4. # Date: Sun Apr : -0400
  5. # interactive rebase in progress; onto 1af1b46
  6. # Last commands done (12 commands done):
  7. # squash a513fd1 Add 'l' to squash.txt
  8. # squash 6b608ae Add 'd' to squash.txt
  9. # No commands remaining.
  10. # You are currently rebasing branch 'squash' on '1af1b46'.
  11. # Changes to be committed:
  12. # new file: squash.txt

保存してエディターを終了し、Gitログを確認してください!

  1. commit cc7dff76f21ce2cad7c51cf2af (HEAD -> squash)
  2. Author: Drew DeVault
  3. Date: Sun Apr : -0400
  4. Add squash.txt with contents "Hello, world"

次に進む前に、行った変更を master ブランチに取り込んでドラフトを削除しましょう。git rebase は git merge と同じように使えますが、マージコミットを作成せずに済みます:

  1. git checkout master
  2. git rebase squash
  3. git branch -D squash

一般に、関係のない履歴を実際にマージするのでなければ git merge の使用は避けたいものです。また、2 つの異なるブランチがある場合は、マージするタイミングを管理するために git merge が非常に便利です。通常の作業では、ベースを変更するほうが適切でしょう。

コミットを複数の

コミットが大きすぎるという逆の問題が起こることもあります。コミットを分割してみましょう。今回は実際のコードを書いてみましょう。まずは簡単なC言語プログラムから始めましょう:

  1. cat <<EOF >main.c
  2. int main(int argc, char *argv[]) {
  3. return 0;

まず提出してください:

  1. git add main.c
  2. git commit -m"Add C program skeleton"

そして、その手順を少し拡大します:

  1. cat <<EOF >main.c
  2. #include &ltstdio.h>
  3. const char *get_name() {
  4. static char buf;
  5. scanf("%s", buf);
  6. return buf;
  7. int main(int argc, char *argv[]) {
  8. printf("What's your name? ");
  9. const char *name = get_name();
  10. printf("Hello, %s! ", name);
  11. return 0;

提出が済んだら、それをどのように分解するかを学びます:

  1. git commit -a -m"Flesh out C program"

最初のステップは、対話的なベース変更を開始することです。以下の varbase プランをもとに、 git rebase -i HEAD~2 2 つのコミットを varbase してみましょう:

  1. pick Add C program skeleton
  2. pick b3f188b Flesh out C program
  3. # Rebase c785f47..b3f188b onto c commands)
  4. # Commands:
  5. # p, pick <commit> = use commit
  6. # e, edit <commit> = use commit, but stop for amending

二番目のコミットのコマンドを pick から edit に変更し、保存してエディターを閉じます:

  1. Stopped at b3f188b... Flesh out C program
  2. You can amend the commit now, with
  3. git commit --amend
  4. Once you are satisfied with your changes, run
  5. git rebase --continue

新しい変更をコミットする方法は後述しますが、git reset HEAD^ を実行することで「ソフトリセット」を行うことができます。そのあとで git status を実行すると、最新のコミットがキャンセルされてその変更が作業ツリーに追加されていることがわかります:

  1. Last commands done (2 commands done):
  2. pick Add C program skeleton
  3. edit b3f188b Flesh out C program
  4. No commands remaining.
  5. You are currently splitting a commit while rebasing branch 'master' on 'c785f47'.
  6. (Once your working directory is clean, run "git rebase --continue")
  7. Changes not staged for commit:
  8. (use "git add ..." to update what will be committed)
  9. (use "git checkout -- ..." to discard changes in working directory)
  10. modified: main.c
  11. no changes added to commit (use "git add" and/or "git commit -a")

分割されるようにするためです。これにより、作業ツリーの特定の変更だけを選択的にコミットできるようになります。git commit -p を実行してこの処理を開始すると、次のようなプロンプトが表示されます:

  1. diff --git a/main.c b/main.c
  2. index b1d9c2c..644
  3. --- a/main.c
  4. +++ b/main.c
  5. @@ -1,3 +1,14 @@
  6. +#include &ltstdio.h>
  7. +const char *get_name() {
  8. + static char buf;
  9. + scanf("%s", buf);
  10. + return buf;
  11. int main(int argc, char *argv[]) {
  12. + printf("What's your name? ");
  13. + const char *name = get_name();
  14. + printf("Hello, %s! ", name);
  15. return 0;
  16. Stage this hunk [y,n,q,a,d,s,e,?]?

Git は、コミットするための「チャンク」をひとつしか与えてくれません。しかし、それでは大きすぎます。そこで、s コマンドを使ってその「塊」を小さな部分に分割してみましょう。

  1. Split into 2 hunks.
  2. @@ -1 +1,9 @@
  3. +#include <stdio.h>
  4. +const char *get_name() {
  5. + static char buf;
  6. + scanf("%s", buf);
  7. + return buf;
  8. int main(int argc, char *argv[]) {
  9. Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?

ヒント:他のオプションが気になる場合は、? サマリー表示。

この塊の方が良さそうです。単一の独立した変更です。yを押して質問に答え、qを押して対話型セッションを終了し、コミットを続けましょう。適切なコミットメッセージを求めるエディターがポップアップします。

  1. Add get_name function to C program
  2. # Please enter the commit message for your changes. Lines starting
  3. # with '#' will be ignored, and an empty message aborts the commit.
  4. # interactive rebase in progress; onto c785f47
  5. # Last commands done (2 commands done):
  6. # pick Add C program skeleton
  7. # edit b3f188b Flesh out C program
  8. # No commands remaining.
  9. # You are currently splitting a commit while rebasing branch 'master' on 'c785f47'.
  10. # Changes to be committed:
  11. # modified: main.c
  12. # Changes not staged for commit:
  13. # modified: main.c

保存してエディターを閉じ、2回目のコミットを実行します。もう一回対話的にコミットを実行することも可能ですが、このコミットでは残りの変更点のみをコミットしたいので、次のようにします:

  1. git commit -a -m"Prompt user for their name"
  2. git rebase --continue

最後のコマンドは Git にこのコミットの編集が終わったことを伝え、次の varbase コマンドに移ります。これで終わりです!git log を実行すると、作業の成果を見ることができます:

  1. $ git log -3 --oneline
  2. fe19cc3 (HEAD -> master) Prompt user for their name
  3. Add get_name function to C program
  4. Add C program skeleton

提出書類の並び替え

とても簡単です。サンドボックスの設定から始めましょう:

  1. echo "Goodbye now!" >farewell.txt
  2. git add farewell.txt
  3. git commit -m"Add farewell.txt"
  4. echo "Hello there!" >greeting.txt
  5. git add greeting.txt
  6. git commit -m"Add greeting.txt"
  7. echo "How're you doing?" >inquiry.txt
  8. git add inquiry.txt
  9. git commit -m"Add inquiry.txt"

これで git log 次のようになります:

  1. f03baa5 (HEAD -> master) Add inquiry.txt
  2. a4cebf7 Add greeting.txt
  3. 90bb015 Add farewell.txt

明らかに、これはすべて順番が狂っています。最後の3つのコミットを対話的にベース変更することで、これを修正しましょう。 git rebase -i HEAD~3実行すると、この varbase プランが表示されます:

  1. pick 90bb015 Add farewell.txt
  2. pick a4cebf7 Add greeting.txt
  3. pick f03baa5 Add inquiry.txt
  4. # Rebase fe19cc3..f03baa5 onto fe19cc3 (3 commands)
  5. # Commands:
  6. # p, pick <commit> = use commit
  7. # These lines can be re-ordered; they are executed from top to bottom.

解決方法は簡単です。コミットを表示したい順番に行を並べ替えるだけです。以下のようになります:

  1. pick a4cebf7 Add greeting.txt
  2. pick f03baa5 Add inquiry.txt
  3. pick 90bb015 Add farewell.txt

保存してエディターを閉じれば、あとは Git がやってくれます。コンフリクトの解決については次のセクションを参照ください。

git pull -rebase

上流で更新されたブランチ <branch> に対してコミットを行った場合、通常 git pull はマージコミットを作成します。この点で、git pull のデフォルトの挙動は同等です:

  1. git fetch origin <branch>
  2. git merge origin/<branch>

ローカル <branch> 元のリモートからのブランチを追跡 <branch> ように設定されているとします:

  1. $ git config branch.<branch>.remote
  2. $ git config branch.<branch>.merge
  3. refs/heads/<branch>

もう1つのオプションがあり、これは通常より便利で、履歴がより明確になりますgit pull --rebaseマージのアプローチとは異なり、これは本質的に以下のものと同じです:

  1. git fetch origin
  2. git rebase origin/<branch>

merge メソッドのほうがシンプルでわかりやすいですが、git rebase の使い方を理解していれば variable-base メソッドでほとんど何でもできるようになります。お望みなら、以下のようにデフォルトの挙動として設定することもできます:

  1. git config --global pull.rebase true

リベースには git rebase を使います。

皮肉なことに、私が一番使っていない Git の changebase 機能が、その名の由来となっている changebase branches です。たとえば次のようなブランチがあったとしましょう:

  1. A--B--C--D--> master
  2. \--E--F--> feature-1
  3. \--G--> feature-2

その結果、feature-2はfeature-1の変更に依存せず、コミットEに依存していることがわかりました:

  1. git rebase --onto master feature-1 feature-2

非対話的な varbase は、feature-1 にない feature-2 からのコミットを master にリダイレクトします。履歴はこのようになります:

  1. A--B--C--D--> master
  2. | \--G--> feature-2
  3. \--E--F--> feature-1

コンフリクトの解決

マージの衝突の解決に関する詳細な情報は、このガイドの範囲を超えています。あなたがコンフリクトを解決する通常の方法に精通していると仮定して、ここでは特に変数ベースに適用されるセクションを示します。

git は影響を受けるファイルにコンフリクトフラグを設定し、git status は解決すべき内容を表示します。そして、git add や git rm を使ってファイルを解決済みとしてマークすることができます。しかし、git のリベースには注意すべきオプションがふたつあります。

まず第一に、コンフリクトの解決方法です。git commit のようなコマンドで git merge によるコンフリクトを解決するのではなく、より適切なリベースコマンドは git rebase --continue です。 しかし、もうひとつ別のオプションがあります。 git rebase --skip です。 これは作業中のコミットをスキップし、リベースには含めません。これは、非対話的なリベースを行う場合によくあることで、"other" ブランチから引っ張ってきたコミットが "" ブランチで競合しているコミットの新しいバージョンであることに Git が気づかないというものです。

助けて 壊しちゃった

ベースを変更するのが難しいことがあるのは間違いありません。ミスを犯して必要なコミットを失ってしまった場合は、git reflog を使用しましょう。このコマンドを実行すると、参照を変更するすべてのアクションが表示されます。各行には、古い参照が指す内容が表示され、git cherry-pick、git checkout、git show、あるいはその他のアクションを実行することができます。

  1. このチュートリアルの残りの部分を簡単にするために、空の初期コミットを追加しました。

  2. このプログラムをコンパイルするには、 \--G--> feature-2実行し、次に./mainを実行して結果を確認してください。

  3. これは実際には「ハイブリッドリセット」です。ソフトリセット」は変更を保持したままなので、git で再度追加する必要はなく、すべての変更を一度にコミットすることができます。これは、あなたが望んでいるものではありません。コミットを分割するために、一部の変更を選択的にステージしたいのです。

  4. git pull --rebase は、git rebase や git merge-base の "" メカニズムを使用してローカルでないコミットのリベースを回避し、この状況から回復しようとします。

  5. 実際には、これはGitのバージョンに依存します。バージョン 2.26.0 までは、デフォルトの非対話的な振る舞いは対話的な振る舞いとは若干異なっていました。

Read next