📖

gitのブランチという概念を初心者が言語化してみた

2024/09/21に公開

書き始めた背景

起きた問題は2つです。

  1. 作業していたブランチの内容が古すぎて、リモートにPUSHしたらCIツールがエラーを検知した。
  2. 作業内容をpushしてプルリクを見てみると、なぜか自分以外のcommitもpushに含まれていてFiles Changeが膨大になっていた。
    3. revertすればいいのでは??と気軽にrevertしたら、「これマージするとこの作業内容自体消されちゃうかもだから気をつけて」と言われたが、よくわからなかった

どちらもgitを理解していないことで発生していたので、
いろんな記事を読んだ上での理解を言語化します。
※自分用の言語化であり、多分図解化とかもしないし読みにくいと思います。

おすすめの記事

本記事は誰かにおすすめできるようなシロモノではないので、今回の理解に役立った記事を以下に記載します。
これらを読めば大枠理解できると思います。
https://qiita.com/t-kubo0325/items/5a2b15cef0aaa92c9713
https://qiita.com/jesus_isao/items/2a0495c973a4c911c2cc
https://www.r-staffing.co.jp/engineer/entry/20190719_1

自分用の言語化

体型立てず、雑に言語化します。

元々のイメージでは、ブランチは「なんかcommitを束ねたもの」だと思っておりました。

しかし実際にはブランチは、「ある1つのcommitを指し示したもの」のような概念でした。
そしてブランチの代わりに、commitそのものが、「束ねた」と同じような役割を持っている。

initial commit以降のcommitは、どのcommitの次に作られたのかを特定するための、一つ前のcommitのコミットIDを保持しています。
そうして連なっていく(=辿れる状態である)commitの中で、どのcommitを最新として持っているかを指し示すのがbranch、というイメージです。
(図解化しないとわかりにくいけど、まあ自分用なので。。)

たとえばmasterブランチの最新のcommitが「コミットA」だとしたら、
「masterブランチ」という概念はそのコミットAを意味するイメージです。

このmasterブランチの中でcommitをし続けると、同時に作業するときに無駄にコンフリクトが起こります。そうならないように、commit内容の保存場所を別で作るのがブランチという概念、みたいなイメージです。

masterにいる状態でgit checkout -b feature/testBranchを行うと、
そのtestBranchは同じく「コミットA」を指し示す概念として誕生する、
その後、testBranchで新しく「コミットB」というcommitを行うと、
testBanchは「コミットB」というcommitを指し示す概念に変わり、「コミットA」までの作業内容は保持しつつ、そしてmasterブランチに影響は与えずに、「コミットB」を保持してくれる感じ。

ではそもそもcommitとは何か。
commitは変更履歴のセーブポイントのような概念です。

ざっくりの流れは以下で、

  1. 作業をしてcommand Sを押すと、ローカルに保存される
  2. そのファイルを指定してgit addを行うと、そのファイルの作業内容がcommitの登録対象=ステージングに上がる
  3. git commitを行うと、git addにて登録された作業内容が保存される。この時点でbranchが指し示す最新のcommitが更新される
  4. さらにgit pushを行うと、リモートリポジトリに対して変更の登録対象になる(git addした状態に近いイメージ)
  5. ここでマージを行うと、merge commitというcommitが発生し、maeterブランチはそのcommitを指し示すように更新される
    みたいなイメージ。

まあ一旦こんなもんにして、コマンドとかケーススタディ的な内容に進みます。

起きていた問題について

  1. 作業していたブランチの内容が古すぎて、リモートにPUSHしたらCIツールがエラーを検知した。
  2. 作業内容をpushしてプルリクを見てみると、なぜか自分以外のcommitもpushに含まれていてFiles Changeが膨大になっていた。
    3. revertすればいいのでは??と気軽にrevertしたら、「これマージするとこの作業内容自体消されちゃうかもだから気をつけて」と言われたが、よくわからなかった

1について

原因は新しくブランチを作るときに指し示していたcommitが古かったことです。
このcommitが古いと、そして特にmerge commitの前だと、いざPUSHしたときにはリモートのブランチが指し示すcommitがかなり進んだものになってしまっているのでエラーが発生します

2について

これはgit checkout -bでブランチを作り直すときに、masterなどではないブランチから分岐をスタートさせてしまったのが要因です。

3について

git revertは、commitした内容の反対の内容をcommitするコマンドです。
「明日は晴れです」というファイルに対して「明日は晴れです。明後日は雨です」という変更を行った(=明後日は雨ですを追加した)コミットAが合った場合、git reverでコミットAを指定すると「明後日は雨です」を削除するという変更内容がcommitされます。
よってこのgit revertによって発生した削除commitをpushやマージしてしまうと、リモートからもそれが削除されることになります。

gitコマンド諸々言語化

雑多に書きます。

git add

作業内容をcommitの登録対象にします。
指定したファイルが前回のcommitと異なる部分をステージングという下書きみたいな状態に登録します。

git commit

ステージングに上がっている内容を全て保存します。
セーブポイントが作られて、いつでもその時点に戻れるようになるイメージです。
バグが発生した時などはどのcommitが原因だったのか特定したりします。

git push

commitをリモートに反映します

git pull

指定したブランチが持っているcommitの内容をローカルに反映します

git stash

commitするとセーブポイントとして登録できますが、たとえばバグが発生したcommitを探すときに、無駄に分割されていると工数が増えます。
そうならないように、中途半端な作業内容をcommitせずに保存するのがgit stashです

git stash save "スタッシュ内容をわかりやすく示すコメント"

コミットメッセージ同様、スタッシュにメッセージをつける機能です。

git stash list

stashの一覧が出力されます。
このときスタッシュメッセージがあると便利です。

git stash apply stash@{0}

stash listで出力されたstash名を指定し、stashが保持している変更内容を取り込むことができます。

git stash drop stash@{0}

apply後で消していいstashなどはこれで消します

$ git stash pop stash@{0}

こうするとapplyとdropを同時に行えます。

git log

今いるbranchが指し示す最新のコミットから順に、コミットを1つずつ遡ってログを出力できるコマンドです。

git reflog

コミットだけでなく、checkoutやpushやaddなど、gitコマンドで行った操作が諸々見られます。

git revert コミットハッシュ値

そのコミットと逆の内容をcommitします
追加した行を削除したり、削除した行を復元したりするcommitを新たに行います

git reset

逆の内容をcommitするrevertとは異なり、その「行動」を行わなかったことにします。
このgit resetの内容もgit reflogを行うと確認できるので、resetに対してresetをかけることもできます。

いくつか種類があります。

git reset -soft コミットのハッシュ値やファイル名やディレクトリ名

commitという「動作」をなかったことにします。
つまり、git addされた状態に戻るので、ステージングされた作業内容自体は保持されます。

git reset -mixed コミットのハッシュ値やファイル名やディレクトリ名

git addという「動作」もなかったことにします。
つまりステージングされる前に戻るので、作業内容自体は保持されます。

git reset -hard コミットのハッシュ値やファイル名やディレクトリ名

作業したこと自体もなかったことにします。
完全にローカルから消え去るので、復元したい時はこのgit resetの操作をgit reflogで発見し、その行動に対してgit resetをかけることなどが必要になります。
しかしそうするとそのgit reflogの行動の前段階に戻るので、その後いろいろ行動してたことも消えます。多分。

git cherry-pick コミットハッシュ値

今いるbranchに、欲しいcommitをコピーしてcommitできます。
しかしたとえばコミットA→コミットB→コミットCの時系列のcommitから、コミットCだけをcherry-pickしても、Cに含まれる作業内容だけが今いるブランチにcommitされるのでAやBを前提とした作業内容がCだった場合は機能しないので要注意です。

HEADとは、今のbranchが指し示している最新のcommitを指します。
HEAD~とすると、HEADから一つ遡ったcommitを指します。
HEAD~~とすると、HEADから二つ遡ったcommitを指します。
よって、たとえばA、B、Cと三つのcommitを行ったブランチでgit push origin headを行うと、今のブランチが示す最新のcommitであるCをpushできます。しかしリモートにまだAやBが存在していない場合は、gitが意を汲んでAもBもpushしてくれるみたいです。
具体的には、git push を実行すると、Gitは以下のプロセスを自動的に行ってくれるそうです。

  1. コミットの比較: Gitは、ローカルブランチの最新のコミット(HEAD が指しているコミット)から遡って、リモートリポジトリに存在しないコミットがあるかをチェックします。
  2. 未プッシュのコミットの識別: リモートリポジトリに存在しないコミット(この場合は A、B)を特定します。
  3. 未プッシュのコミットをプッシュ: HEAD が指している最新のコミット(この場合は C)に至るまでのすべての未プッシュのコミット(A と B)を一緒にプッシュします。
    便利ですね。

GPTにいろいろ解説してもらった

git checkout -b

質問

git checkout -bを行った時のgitの具体的な処理内容を詳しく教えてください。

回答

git checkout -b <新しいブランチ名> コマンドを実行すると、Gitは以下の具体的な処理を行います。

  1. 新しいブランチの作成
    現在のブランチのコミットを参照: 新しいブランチを作成すると、現在のブランチが指している最新のコミットを参照します。このコミットが新しいブランチの親コミットになります。
    ブランチ名の登録: 指定された 新しいブランチ名 で、refs/heads/ ディレクトリに新しい参照(リファレンス)を作成します。これにより、新しいブランチが作成され、そのブランチは現在のブランチの最新のコミットを指します。
    例: もし現在のブランチが main で、その最新のコミットのハッシュが abc123 である場合、git checkout -b feature-branch を実行すると、feature-branch という新しいブランチが abc123 を指すように設定されます。

  2. 新しいブランチへの切り替え
    HEAD の更新: Gitは現在の HEAD を新しいブランチに移動します。これにより、HEAD は今作成された新しいブランチ(例えば feature-branch)を指すようになります。HEAD が新しいブランチを指すことで、以後のコミットはこの新しいブランチ上に追加されることになります。

  3. 作業ディレクトリの更新
    ファイルの更新: 新しいブランチに基づいて、作業ディレクトリ内のファイルを更新します。通常、新しいブランチは現在のブランチから作成されるため、作業ディレクトリの内容に変化はありません。しかし、もしブランチ間でファイルの違いがある場合は、適切にファイルが更新されます。

  4. ステージングエリアの保持
    ステージングエリアの状態: ステージングエリア(インデックス)の内容は、ブランチを切り替えてもそのまま維持されます。つまり、新しいブランチに切り替えた後でも、前のブランチでステージングしていたファイルはそのままステージングエリアに残ります。

  5. ログや設定の更新
    ブランチ情報の記録: Gitはブランチの切り替え操作を履歴に記録します。これにより、git reflog コマンドでブランチの切り替え履歴を確認することができます。
    処理の流れのまとめ
    現在のブランチの最新コミットを参照して、新しいブランチが作成されます。
    HEAD が新しいブランチを指すように移動します。
    作業ディレクトリが新しいブランチに基づいて更新されます(通常は変更なし)。
    ステージングエリアの状態はそのまま保持されます。
    この一連の処理により、新しいブランチが作成され、同時にそのブランチに切り替わります。

HEADという概念

質問

HEADって何ですか?

回答

HEAD は、Gitにおいて現在チェックアウトされているブランチやコミットを指す特別なポインタ(参照)です。具体的には、現在の作業ディレクトリがどのコミットを基準にしているかを示しています。

詳しい説明

現在のブランチ: 通常、HEAD は現在チェックアウトしているブランチを指します。たとえば、main ブランチがチェックアウトされている場合、HEAD は refs/heads/main を指しています。この状態では、HEAD が指しているブランチの先端(最新のコミット)が作業の基準になります。

現在のコミット: もし特定のコミットを直接チェックアウトした場合(例: git checkout <commit-hash> を実行した場合)、HEAD はそのコミットを指すようになります。この状態を「detached HEAD(デタッチドヘッド)」と呼びます。この場合、HEAD はブランチではなく、特定のコミットを指しているため、その状態で新しいコミットを作成しても通常のブランチ上には追加されません。

HEAD の役割
作業ディレクトリの基準: HEAD が指しているコミットを基準に、作業ディレクトリの内容が構成されます。ファイルの内容やステージング状態はこの HEAD に基づいて管理されます。

ブランチの切り替え: git checkout <ブランチ名> コマンドを実行すると、HEAD はそのブランチに移動し、そのブランチが指している最新のコミットが作業ディレクトリの基準になります。

コミットの追跡: 新しいコミットを作成すると、そのコミットは現在のブランチの先端に追加され、HEAD はその新しいコミットを指すようになります。

HEAD の状態
アタッチド(attached)HEAD: 通常の状態で、HEAD がブランチを指しているとき。
デタッチド(detached)HEAD: HEAD がブランチではなく、特定のコミットを直接指しているとき。この状態では、コミットをしてもブランチの先端には追加されません。

まとめ

HEAD は、現在の作業基準となるコミットやブランチを指しているGitのポインタであり、作業ディレクトリやステージングエリア、コミット操作において非常に重要な役割を果たしています。

cehckoutという概念

質問

checkoutって何ですか?

回答

git checkout は、Gitで使用されるコマンドの一つで、特定のブランチやコミット、ファイルに作業ディレクトリを切り替えるために使用されます。これにより、異なるブランチ間での移動や、過去の特定のコミットに基づいた作業ができるようになります。

git checkout の主な用途

  • ブランチの切り替え:
    git checkout <ブランチ名> を実行すると、指定したブランチに切り替わります。このとき、HEAD はそのブランチを指し、作業ディレクトリの内容がそのブランチの最新のコミットに基づいて更新されます。
    例: git checkout main とすると、main ブランチに切り替わります。

  • 新しいブランチの作成と切り替え:
    git checkout -b <新しいブランチ名> を実行すると、指定した名前の新しいブランチが現在のブランチから派生して作成され、同時にその新しいブランチに切り替わります。
    例: git checkout -b feature-branch とすると、feature-branch という新しいブランチが作成され、そのブランチに切り替わります。

  • 特定のコミットのチェックアウト:
    git checkout <コミットハッシュ> を実行すると、指定したコミットに基づいて作業ディレクトリが更新されます。この場合、HEAD はそのコミットを直接指すようになり、「デタッチドヘッド (detached HEAD)」と呼ばれる状態になります。
    例: git checkout abc1234 とすると、コミット abc1234 に基づいた作業ディレクトリに切り替わります。

  • ファイルの復元:
    git checkout -- <ファイル名> を実行すると、指定したファイルを最後のコミット時の状態に戻します。これにより、作業ディレクトリの変更が取り消され、ファイルがコミット時の内容に復元されます。
    例: git checkout -- README.md とすると、README.md ファイルが最後にコミットされた内容に戻ります。
    git checkout の使用例
    ブランチの切り替え: git checkout develop
    新しいブランチの作成と切り替え: git checkout -b feature/login
    過去のコミットに切り替え: git checkout 1a2b3c4d
    ファイルの変更を取り消す: git checkout -- index.html
    git checkout のまとめ
    ブランチやコミットの切り替え: 別のブランチや過去の特定のコミットに基づいて作業ディレクトリを切り替えます。
    ファイルの復元: コミット時の状態にファイルを戻します。
    新しいブランチの作成: 現在のブランチから新しいブランチを作成し、すぐにそのブランチに切り替えます。
    このように、git checkout はGitの操作の中で非常に重要なコマンドであり、ブランチ管理や過去の履歴の確認に欠かせない機能を提供します。

質問

git checkoutはHEADが指し示すモノを切り替えられるイメージですか?
そして通常HEADはbranch、つまりそのbranchが指し示す最新のcommitを指し示すけど、branch内で最新じゃないcommitを指し示すこともできる?

回答

はい、そのイメージは正しいです。

git checkout と HEAD の関係

HEAD の役割: HEAD は現在の作業ディレクトリが基づいているブランチやコミットを指すポインタです。通常、HEAD は現在チェックアウトしているブランチを指し、そのブランチの最新のコミットを基準に作業を行います。

git checkout の役割: git checkout コマンドを使用すると、HEAD が指し示すものを切り替えることができます。通常はブランチ間の切り替えに使用されますが、特定のコミットに切り替えることもできます。

HEAD の通常の状態

ブランチを指している場合: 通常、HEAD はブランチを指しています。たとえば、main ブランチがチェックアウトされている場合、HEAD は main ブランチの最新のコミットを指します。このとき、作業ディレクトリは main ブランチの先端(最新のコミット)に基づいています。

ブランチ内で最新ではないコミットを指し示すこと

HEAD を特定のコミットに切り替える: git checkout <コミットハッシュ> を実行することで、そのコミットに HEAD を切り替えることができます。この場合、HEAD はブランチではなく、そのコミット自体を直接指します。この状態を「デタッチドヘッド (detached HEAD)」と呼びます。
例: git checkout abc1234 を実行すると、コミット abc1234 に HEAD が切り替わり、そのコミットを基準に作業ディレクトリが更新されます。この状態では、そのコミットが属するブランチの最新コミットではない可能性があります。

detached HEAD の状態

detached HEAD とは: HEAD がブランチではなく、特定のコミット(ブランチの最新ではない場合も含む)を指している状態です。この状態では、新しいコミットを行ってもブランチには追加されず、特定のコミットから派生した履歴が作成されます。もしこの状態で作業を続けたい場合、通常は新しいブランチを作成することが推奨されます(git checkout -b <新しいブランチ名> などを使って)。

まとめ

git checkout は HEAD が指すものを切り替える: HEAD をブランチや特定のコミットに切り替えるために使用します。
通常 HEAD はブランチの最新のコミットを指す: ただし、特定の過去のコミットを指すように HEAD を切り替えることも可能です(この状態を detached HEAD と呼びます)。
したがって、git checkout を使うことで、ブランチの最新のコミットだけでなく、そのブランチ内の任意のコミットに切り替えて作業を進めることができます。

質問

HEADは基本的に「何に対して作業を行うか」を指定するポインターというイメージで合ってる?

回答

はい、そのイメージは正しいです。

HEAD は、Gitにおいて「現在何に対して作業を行っているか」を指定するポインター(参照)です。具体的には、HEAD が指しているブランチやコミットが、あなたの作業ディレクトリやステージングエリアが基づいている「基準」となります。

HEAD の基本的な役割

ブランチを指す場合: 通常、HEAD は現在作業しているブランチを指します。たとえば、HEAD が main ブランチを指している場合、すべてのコミットやファイルの変更は main ブランチに対して行われます。

コミットを指す場合: HEAD が特定のコミット(ブランチの最新ではない場合も含む)を指していると、そのコミットを基準に作業を行うことができます。この状態では、新しいコミットは通常のブランチに追加されず、独立した履歴を作成することになります。

まとめ

HEAD は「何に対して作業を行うか」を指定するポインターであり、その指し示す対象によって、あなたの作業の基準が決まります。このため、HEAD が指しているブランチやコミットを切り替えることによって、過去の状態に戻って作業を行ったり、別のブランチで新しい作業を開始したりすることが可能です。

おわり

だいぶ解像度が上がりました。
git ってcommitが主役で、それ以外は主役を輝かせる引き立て役って感じですね(雑)

Discussion