Git の履歴の操作

学習の目的

この単元を完了すると、次のことができるようになります。

  • どのようにして Git がデータを保存しているのかを説明し、そうした知識を実際に活用する方法を挙げる。
  • プロジェクトの履歴と変更を Git に表示する。
  • 以前の変更を元に戻す Git コマンドを実行する。
  • リベースと一般的なマージ戦略との関連性について概説する。

Git でのデータの保存方法

以前コミットについて説明したとき、コミットはプロジェクトのスナップショットであると述べました。どのスナップショットにも多くの情報が含まれています。スナップショットに圧縮されたファイルにはそれぞれ、ブロブという一意の SHA-1 ハッシュが与えられます。ツリーはこれらのブロブを参照し、そのツリーをコミットが参照します。Git のコミットツリーの図

ファイルに変更を加えて新しいコミットを作成すると、Git が変更されたファイルを識別して新しい SHA-1 ハッシュをそのファイルに適用します。一方、変更されていないファイルについては既存の SHA-1 ハッシュを維持します。SHA-1 ハッシュが変更されたファイルは、以前の SHA-1 ハッシュを親として参照します。GitHub では、各コミットに与えられた SHA-1 ハッシュ (A) の最初の 7 文字を表示できます。

メモ

メモ

現在、SHA-1 ハッシュが使用されていますが、Git プロジェクトは今後新しいハッシュアルゴリズムを選択することを決定しました。Git は SHA-1 の後継として SHA-256 を選択し、この移行に向けて現在作業が進んでいます。

これは、コミット履歴の処理方法とよく似ています。新規作成されたコミットは、以前のコミットを親として参照します。この参照ポイントはきわめて重要です。この線形の親子関係に基づいて一貫した履歴が作成され、Git がブランチをまとめてマージできるようになるのです。

Git での履歴の探索

プロジェクトで作業中にコミット履歴を確認できれば便利ですよね。GitHub.com では、プロジェクトのコードタブにあるコミットボタンを選択すると、プロジェクト履歴にアクセスできます。ローカルでは、git log を使用します。

git log コマンドを実行すると、現在のブランチのすべてのコミットがリストされます。デフォルトでは、git log コマンドによって多くの情報が一度に表示されます。

以下の git log の修飾子を使うと、有益な情報のまとまりとして、見やすいリストを作成できます。

  • git log -10 は、最近の 10 件のコミットのみを表示します。
  • git log --oneline は、コミット履歴を表示する優れた方法です。現在のブランチのコミットに与えられた SHA-1 ハッシュの最初の 7 文字とコミットメッセージを表示します。
  • git log --oneline --graph は、コミット履歴を ASCII グラフに表示し、リポジトリの各ブランチとそのコミットを示します。
  • git log --oneline --graph --decorate は、--graph 修飾子を使用した場合と同じ ASCII グラフに加えて、各コミットのブランチ名も表示します。

「git log --oneline --graph --decorate」コマンドの出力を示すスクリーンショット。

ファイルのバージョンの比較

完璧なコミットを作成しようとするときに、ワーキングディレクトリとステージングエリアの現在のコンテンツの差異を表示できれば、git add で適切なファイルをコミットに追加できます。

デフォルトでは、git diff コマンドを使用して、プロジェクトの最後のコミットとファイルのさまざまな状態 (ワーキングディレクトリにある場合やステージングエリアにある場合) 間の変更を確認できます。 

ワーキングディレクトリ、ステージングエリア、履歴の変更を比較する Git の差分オプションの図。

また git diff を使用して、リポジトリ内の 2 つのコミット、ブランチ、タグなどを比較することもできます。たとえば、SHA-1 ハッシュの参照が 4e3dc9b0cd75d4 である 2 つのコミットを比較するには「git diff 4e3dc9b 0cd75d4」というコマンドを入力します。

さらに、以前のコミットに加えた変更を表示する場合は、 git show <SHA-1> コマンドを使用すると特定のコミットの詳細を表示できます。このコマンドで、コミットの著者、コミットの日時、リポジトリ内の各種アセットに加えられた変更のリストなどが示されます。

「git show <SHA-1>」コマンドの出力を示すスクリーンショット。

以前の変更を元に戻す

私たちはちょっとした間違い (あるいは大変な間違い) をしてしまうことがあります。幸い、Git には間違いを修正できるコマンドが用意されています。ただし、間違いを修正できるコマンドの中には、コミット ID を破壊的に変更してしまうものがあります。コミット ID は不変なため、他のコラボレータに問題が生じる可能性があります。原則として、git revert の使用は、すでにコミットをリモートにプッシュしている場合に限定します。

変更を元に戻す
git revert は、コミットの逆の変更を加える新しいコミットを作成します。これは機能的に「元に戻す」ことになります。

たとえば、リポジトリのコミット履歴内に、ヘッダー 3 をすべてヘッダー 5 に変更するコミットがあるとします。git revert を使用すると、ヘッダー 5 を 1 つずつヘッダー 3 に戻すのではなく、ヘッダーをまとめてヘッダー 3 に戻すことができます。

git revert は、リポジトリの履歴の任意時点のコミットに使用でき、他の作業に影響が及ぶことがありません。ただし、git revert で競合を解決することはできません。競合する変更が新たに生じた場合は、マージ競合と同じく、解決するように指示されます。

git revert は、特定の変更を安全に元に戻す手段です。これによって、プロジェクトのコミット履歴を変更するときに生じる、一般的かつ複雑な問題が回避されます。

コミットの修正
以前、git commit について説明したとき、詳細なプロジェクト履歴の作成においてコミットメッセージが重要な要素であると述べました。

時として、張り切ってコミットを作成している途中で、コミットメッセージに入力ミスをしてしまうことがあります。git commit --amend を使用すれば、最後に行ったコミットを修正できます。この操作によってプロジェクトのコミット履歴が修正されてしまうため、すでにコミットをリモートにプッシュしている場合は、git commit --amend を使用しないことをお勧めします。

また git commit --amend で、最新のコミットを書き換えてステージングエリアにファイルを追加することもできます。

履歴の以前の時点に戻す

周知のとおり、プログラマは新しいことを試したがります。ただ、試行を繰り返す中で間違った方向に進んでいることや、作成しているコミットが思いのほか役に立たないことに気づく瞬間もあります。

Git にはプロジェクトの履歴を元に戻す git reset コマンドがあります。ただし、コミット履歴が修正されてしまうため、前述のとおり、他のコラボレータに問題が生じる可能性があります。git reset の使用は、コミットをまだリモートブランチにプッシュしていない場合に限定することを強くお勧めします。git reset には、--soft--mixed--hard の 3 種類があります。

  • git reset --soft は、指定したコミットを取得して、すべての変更をステージングエリアに配置します。これは、一連のコミットを取得して、1 つの大きなコミットにまとめたい場合に役立ちます。
  • git reset --mixed は、git reset のデフォルトモードで、指定したコミットを取得して、すべての変更をワーキングディレクトリに配置します。--soft と同様、一連の小さなコミットを取得し、いくつかの変更をまとめて大きなコミットにしたい場合に役立ちます。また、ファイルにさらなる変更を加えて、コミット履歴を再作成する場合にも使用できます。
  • git reset --hard は、指定したコミットを取得して破棄します。このコマンドは慎重に使用します。破棄されたファイルはごみ箱に入れられることなく、リポジトリから完全に削除され、実質的に存在しなくなります。まだファイルにコミットされておらず、現在ワーキングディレクトリまたはステージングエリアにある変更も削除されます。git reset --hard を実行して作業が失われることがあります。

reset の使用例として次のような状況が挙げられます。

git reset --soft HEAD~2 を実行すると、作業中のブランチが 2 つ前のコミットに戻されます (HEAD は前述のとおり、ブランチの先頭へのポインタです)。最後 2 回のコミットで加えられた変更はステージングエリアに反映されます。

Git のマージ戦略

Git のマージについては、変更をマスタブランチ (あるいはマージ先のブランチ) に適用する主な方法として、再帰的マージと早送りマージの 2 種類があります。

どちらかのマージ戦略が正しいあるいは間違っているということはなく、各戦略が履歴の表示にどのように影響するのかを認識しておくことが重要です。一見すると、これらはマージ戦略というよりも、単に Git がマージを処理するための方法を表しているように思えるかもしれません。2 つのマージについて説明した後、その点についても解説します。 

再帰的マージ
再帰的マージが生じるのは、機能ブランチに、マージ先ブランチ (通常はマスタですが、そうでない場合もあります) のコードの最新バージョンが存在しない場合です。

再帰的マージの図

機能ブランチを作成するときは、元のブランチの現在の状態を基にします。ブランチに変更を加えた時点で、他のコラボレータが各自の変更を元のブランチにマージしている可能性があります。

プル要求を作成して変更をマージすると、マージコミットが作成されます。この操作によってブランチに加えた変更と、マージ先ブランチの現在の状態が取得され、これらの変更を組み合わせた新しいコミットが作成されます。

早送りマージ
早送りマージが生じるのは、元のブランチから機能ブランチを作成した後、元のブランチに、マージしようとしているコミット以外に新しいコミットが存在しない場合です。

早送りマージの図

元のブランチに変更がないため、ブランチの先頭が単純に早送りで移動していき、ブランチの変更が取り入れられます。早送りマージの場合は、Git で新しいマージコミットが作成されません。

再帰的マージを早送りマージに変換
git rebase は強力なコマンドで、リポジトリで多数の優れた操作を実行できます (このすべての操作で履歴が書き換えられるため慎重に使用します)。git rebase のごく一般的な用途の 1 つが、Git でデフォルトの再帰的マージが設定されようとしたときに、早送りマージを作成することです。 

前述のとおり、再帰的マージが生じるのは、ブランチを作成した後にマージ先ブランチに新しい変更が存在する場合です。リベースによって、ブランチに行ったコミットが取得され、選択したブランチの最後のコミットの後に適用されます。また git rebase を使用して、コミット履歴の表示を変更することもできます。rebase に -i のインタラクティブ修飾子を設定すると、以前のコミットメッセージを編集して、コミットを大きなコミットにまとめ、コミット履歴を並び替えることができます。

リソース