世界一わかりみが深いかもしれないGit

Git workflow diagram: main is merged into develop (fast-forward) with commits A–G and labeled steps (Japanese captions).

こんにちは、サイオステクノロジー武井です。

今回は、100番煎じくらいかもしれない、Gitの説明です。

Git(ギット)は、ファイルの変更履歴を記録・管理するためのツールです。チーム開発では欠かせませんが、入門者にとっては最初の壁がとても高いツールでもあります。

その理由のひとつが、コマンドの多さです。addcommitpushfetchmergebranchcheckoutswitchresetrebase……。これらを1つずつ「使い方」として暗記しようとすると、たいていどこかで混乱してしまいます。たとえば checkout はブランチの切り替えにもファイルの破棄にも使われますし、reset  --soft / --mixed / --hard の違いはなかなか覚えられません。

でも、わたしも多分10年くらいGitと付き合っているかもしれませんが、以下のことだけ覚えればOKだと思っています。

Git のコマンドは、結局のところ「ファイルの場所」と「コミット(履歴)の動き」の2つを変えているだけ。

わたしはGitのコマンドの使い方を多分一つも覚えてないと思います。でも上記の2つのことを理解していれば、多分、どんな操作も自然に理解できるようになります。

こちらでは、繰り返しになりますが、「Git のコマンドは、結局のところ「ファイルの場所」と「コミット(履歴)の動き」の2つを変えているだけ。」をベースにしつこく説明していきます。


目次

第1章 コミットの仕組み ── すべての土台

Git を理解する出発点は、コマンドではなく「コミットとは何か」です。ここがあいまいなまま push  merge を覚えても、どこかでつまずいてしまいます。逆に、コミットの正体さえ分かってしまえば、残りは驚くほど素直につながっていきます。少し遠回りに感じるかもしれませんが、まずはここをじっくり見ていきましょう。

なお、これから何度か出てくる言葉を先に確認しておきます。

  • リポジトリ:変更履歴をまるごと保管しておく入れ物のことです。
  • コミット:ある時点のファイルの状態を記録する操作、またはその記録そのものを指します。

コミットは「差分」ではなく「スナップショット」

コミットについて、多くの方が最初に思い描くのは「コミット=変更した部分(差分)の記録」というイメージではないでしょうか。じつは、これは少し違います。

正しくは、コミットは、その時点のプロジェクト全体の“写真(スナップショット)”を、まるごと内側に抱えています。「どこを変えたか」ではなく、「その瞬間、全体がどうなっていたか」を丸ごと持っている、とイメージしてください。

 

図のように、コミットは入れ子(箱の中に箱)の構造になっています。

  • コミットは、その時点のツリー(ルートフォルダの構造)を内側に持っています。
  • ツリーは、その中のフォルダ(A/B/)やファイル(C.txtD.txtE.txt)の実体を内側に持っています。
  • そして ブランチ(main は、その箱(コミット)を外から指し示している名札のようなものです。

ここで、2種類の「登場人物」を区別しておくと、このあとがずっと楽になります。

種類どんなもの?
Git オブジェクト中身そのもの。中身から計算される「ハッシュ値(指紋のような文字列)」で区別され、一度作ると中身は変わりませんコミット・ツリー・ファイルの実体
リファレンスコミットを指す「名札(ポインタ)」。中身は持ちませんmaindevelopHEAD

「中身を持つもの(オブジェクト)」と「それを指すだけの名札(リファレンス)」を分けて考える ── この見方は、このあと何度も登場します。最初はピンとこなくても大丈夫です。読み進めるうちに、だんだんなじんでいきます。

「毎回スナップショットを撮ったら、重くならない?」

ここで、素朴な疑問が浮かぶかもしれません。コミットのたびにフォルダ全体の写真を撮るのなら、ファイルがどんどん増えて、すぐに膨大になってしまうのでは?

とても良い疑問です。でも、実際にはそうはなりません。Git は、**変わったファイルだけを新しく作り、変わっていないファイルは前のコミットの実体をそのまま使い回す(共有する)**ようにできているからです。

E.txt だけを編集してコミットした場面を見てみましょう。

  • E.txt は中身が変わったので、新しいオブジェクトが作られます。
  • A/B/C.txtD.txt 1文字も変わっていないので、新しいコミット B は、これらを自分で持たず、前のコミット A が持っている実体をそのまま指すだけです(図の紫の矢印)。コピーは作られません。一方、変わった E.txt だけは A を参照せず、新しいオブジェクトを作ります。
  • コミット B は、自分の親(一つ前)としてコミット A を指します(parent)。こうしてコミットがつながり、鎖(くさり)のような履歴ができていきます。

なぜ「変わっていないものは共有」できるのでしょうか。それは、オブジェクトが「中身から計算したハッシュ値」で区別されるからです。中身が同じなら、ハッシュ値も同じ=同じオブジェクトとして扱われます。だから、同じ内容のものは自然と1つにまとまります。コミットが毎回スナップショットでも軽いのは、このおかげです。

この「中身は変えず、新しいオブジェクトを作って、指し先を付け替えるだけ」という性質は、あとの章に出てくる話(rebase でハッシュが変わる理由や、消したはずのコミットが残る理由)にもつながっていきます。少し頭の片隅に置いておいてください。

もう一歩だけ深く ── tree と blob の正体

ここまで「コミット → ツリー → ファイル」と入れ子で説明してきました。じつは、この“中身”は Git の内部では 2種類のオブジェクトでできています。少しだけ専門的な話になりますが、ここが分かると Git の不思議な動きの多くが「なるほど」に変わるので、ゆっくり見ていきましょう。

オブジェクトどんなもの?
blob(ブロブ)ファイルの中身(データそのもの)
tree(ツリー)「名前 → オブジェクト」の一覧表。これがいわゆるディレクトリ(フォルダ)にあたります

図のように、tree は mode(権限)/ type(種類)/ object(ハッシュ)/ name(名前) という列を持つ一覧表です。その各行が、blob(ファイル)か、別の tree(サブフォルダ)を指しています。

この仕組みから、最初は意外に感じる事実がいくつも見えてきます。

  • blob は中身しか持っていません。 ファイル名も、どのフォルダにあるかも、権限も、いっさい持っていません。ただのデータの固まりです。
  • 名前・場所・権限は、すべて tree のほうが持っています。 C.txt という名前で、ここにある」という情報は、blob ではなく tree 側にあります。
  • 指し示す向きは commit → tree → blob の一方向だけです。逆向きの矢印(戻りリンク)はありません。つまり、blob は自分がどこに置かれているかを知りません
  • だからこそ、中身が同じファイルは、名前や場所が違っても1つの blob を共有します(図の C.txt  notes.txt は同じハッシュ=同じ実体です)。前の節で見た「変わらないものは共有」は、この性質そのものです。

よくある疑問:「tree は編集できるの? 管理(追跡)の対象なの?」

入門者の方からよくいただく質問です。結論から言うと、**どちらも「いいえ」**になります。

  • Git のすべてのオブジェクトは、一度作ったら中身が変わりません(これを「不変=イミュータブル」と呼びます)。中身が少しでも変われば、それは別のハッシュ値を持つ「別のオブジェクト」になります。ですから、tree も blob も「編集」はできず、変更とは常に新しいオブジェクトを作り直すことになります。
  • 追跡(管理対象にする)」という言葉は、後ほど出てくる「ステージング」という場所の話で、そこで管理されるのはファイルだけです。tree は直接そこに登録されるものではなく、コミットする瞬間に、管理中のファイル一覧から自動的に組み立てられます。ですから「tree を追加する/管理する」という操作は、そもそも存在しません。
  • その結果、ファイルを1つ変えると、その影響は下から上へ伝わっていきます。変更したファイルから一番上(ルート)までの道すじにある tree が、すべて新しく作り直されます。一方、その道すじから外れたフォルダ(A/B/ など)は、前の実体をそのまま共有します。

ディレクトリ(フォルダ)の正体も tree です。そのため、Git は空のフォルダを記録できません(一覧表に載せるファイルが1つも無いためです)。空フォルダを残しておきたいときに .gitkeep のような中身のないファイルを置くのは、これが理由です。

では「フォルダを削除する」と、内部では何が起きる?

フォルダ(ディレクトリ)は単独では管理されず、中のファイル(blob)があって初めて tree の一覧に載るのでした。ですから、git rm -r A/A/ フォルダを削除するコマンド)の実体は、「A/ の中のファイルを一覧から全部外す」ことになります。

  • コミットすると、残ったファイルから新しいルート tree が組み立てられます。そこには、もう A/ を指す行が含まれません。これが「削除」の正体です。「フォルダを消す」専用の操作があるわけではなく、「新しい tree が、それを指さなくなるだけ」なのです。
  • ところが、A/ の tree や、その中の blob は、すぐに消えるわけではありません。オブジェクトは不変なので、一つ前のコミット(Commit B)からは、今でもたどり着けるからです。過去のコミットに切り替えれば、フォルダはちゃんと元どおりに戻せます。
  • 本当に消えるのは、どのコミットからも・どのブランチからもたどり着けなくなったオブジェクトを、git gc(ガベージコレクション。不要なものを片づける処理)が掃除するときだけです。

これは「秘密情報(パスワードなど)を含むファイルを、あとから削除しても、過去のコミットに残り続けてしまう」という、セキュリティ上のとても大切なポイントと同じ仕組みです。一度コミットしてしまった秘密は、削除では消えたことになりません。漏れてしまった場合は、削除に頼るのではなく、そのパスワード自体を無効化・変更するのが正しい対処です。

ここまでを一度まとめておきます。

blob は「名前を持たない中身」、tree は「名前と場所を持つ一覧表」。どちらも一度作ると変わらず、変更も削除も“新しいオブジェクトを作って、指し先を付け替える”形でしか起きません。だから、同じ中身は共有され、消したはずのものは履歴に残るのです。

この「変えずに、新しく作って、指し先を付け替える」という土台が、このあとのブランチ・マージ・reset・rebase まで、すべての動きを支えています。

ブランチは「コミットを指す軽い名札」

ここが、この記事でいちばんお伝えしたい転換点です。

「ブランチ」と聞くと、コードの束や、フォルダのコピーのような“重たいもの”を想像しがちです。私自身も最初はそう思っていました。でも、本当はとてもシンプルです。

ブランチとは、あるコミットを指しているだけの、軽い名札(ポインタ)です。

  • git branch develop(develop というブランチを作るコマンド)を実行しても、新しいコミットは1つも作られませんdevelop という名札が1枚増えて、今のコミットを指すだけです。だから、ブランチを作る操作はとても軽く、一瞬で終わります。
  • そのブランチで新しくコミットすると、名札が新しいコミットのほうへ進みます
  • HEAD(ヘッド)は「いま自分がどこにいるか」を指す特別な名札です。ふだんはブランチ(名札)を経由して、コミットを指しています。

ブランチ=動く名札」という見方ができるようになると、見える景色が変わります。resetcheckoutmergerebase といった難しそうなコマンドも、すべて「名札をどう動かすか」という同じ視点でとらえられるようになるのです。

紙芝居で見る ── 1コマンドずつ、履歴が育っていく

ここからが、第1章の本番です。「コミット=スナップショット」「ブランチ=名札」「HEAD=いまいる場所」という3つの道具を手に持ったまま、実際にコマンドを1つずつ打って、履歴がどう育っていくかを、紙芝居のように1コマずつ見ていきましょう。

見るポイントは、いつも同じ3つだけです。

  1. 新しいコミットができたかな?(緑=ふつうのコミット/紫=マージコミット)
  2. どのブランチ(名札)が、どこへ動いたかな?
  3. HEAD(いまいる場所)は、どこを指しているかな?

この3点をその都度確認していけば、どんなに枝分かれしても迷子になりません。では、始めましょう。


STEP 1 最初のコミット

リポジトリを作って、最初のコミット A を記録した状態です。main  A を指し、HEAD は「いま main にいますよ」ということを表しています。コミット → ブランチ → HEAD という指し示しの流れが、すべての出発点になります。

STEP 2 2回目のコミット

E.txt を変更してコミットすると、新しいコミット B ができます。B の親(一つ前)は A です。そして、いま自分がいるブランチ main  B へ進み、HEAD も一緒について動きます

第1章の前半を思い出してください。B の中では E.txt だけが新しいオブジェクトになり、A/B/C.txtD.txt  A の実体をそのまま共有していましたね。紙芝居では1つの丸で表していますが、丸の中身は、あのスナップショットになっています。

STEP 3 ブランチを作る

git branch develop を実行します。ここで注目していただきたいのは、コミットが1つも増えていないことです。develop という名札が1枚増えて、今のコミット B を指すだけです。そして、HEAD はまだ main のままで、動いていません。「ブランチを作ること」と「そのブランチに移ること」は、別々の操作なのです。

STEP 4 develop に切り替えてコミット

まず git checkout develop  HEAD を develop へ移します(この瞬間は、まだコミットは増えません)。それから C.txt を変更してコミットすると、C ができます。

ここが紙芝居の最初の山場です。いま自分がいるブランチ develop だけが C へ進み、main  B に取り残されます。これが「枝分かれ」の正体です。特別な操作は何もなく、「動くのは、いま自分がいるブランチだけ」というルールから自然に生まれているだけなのです。

STEP 5 featureA を作る

git branch featureA を実行します。STEP 3 と同じく、いまの HEAD の位置(develop  C)に名札を1枚足すだけです。コミットは増えず、HEAD も develop のままです。これで C には、develop  featureA の2枚の名札が貼られた状態になります。

STEP 6 featureA でコミット

featureA に切り替えてコミットすると D ができ、featureA だけが D へ進みますdevelop  C に残ったままです。これで、同じ C を起点に、develop のラインと featureA のライン、2本の流れに枝分かれしました。

STEP 7 develop に戻って featureB を作る

git checkout develop で HEAD を C に戻し、そこで git branch featureB を実行します。これで C には develop  featureB の名札が、featureA の先には D が、という構図ができあがりました。名札は、どのコミットにでも、何枚でも貼れます

STEP 8 featureB でコミット

featureB に切り替えてコミットし、E を作ります。C から下へ枝分かれした、3本目のラインです。いま動いている開発の流れは、main(B)・develop(C)・featureA(D)・featureB(E)の4本になりました。どれも「コミット=スナップショット」と「ブランチ=名札」の組み合わせでできています。

STEP 9 featureA でさらにコミット

featureA に戻って、もう一度コミットします。D の上に F が積まれ、featureA  F へ進みます。featureA のラインは C → D → F という、3つのコミットの鎖になりました。

STEP 10 develop に featureA をマージ

いよいよ、2つの流れの合流(マージ)です。develop に切り替えて git merge featureA を実行します。

このとき、develop の現在地は CfeatureA の先端は F です。この2つを合流させるために、親を2つ持つ「マージコミット」G が新しく作られます(図の紫の丸です)。G の親は C(合流先)と F(取り込む側)の2つです。そして develop  G へ進みますfeatureA  F のまま、動きません。

マージとは「2つの流れの合流点に、親を2つ持つコミットを作る」操作です。難しく考える必要はありません。やっていることは、やはり「コミットを作って、いまいるブランチを進める」だけです。

STEP 11 main に develop をマージ(早送り)

最後に、main に切り替えて git merge develop を実行します。

このとき、main の現在地 B は、develop の現在地 G 祖先になっています(B → C → G とたどれます)。こういう場合は、新しいマージコミットを作る必要がありません。main の名札を、そのまま G までスッと滑らせるだけで済みます。これを fast-forward(早送り) と呼びます。

その結果、main  develop が同じ G を指して、きれいに揃いました。featureA(F)と featureB(E)の名札は、そのまま残っています。


第1章のまとめ ── 11ステップで起きていたこと

紙芝居を最初から見返してみると、たった2種類のことしか起きていないことに気づきます。

  • コミットを作る(STEP 2・4・6・8・9・10)→ 新しいスナップショットができ、いま自分がいるブランチが、そこへ進む
  • 名札を動かす(STEP 3・5・7 のブランチ作成、STEP 4・7・10・11 の切り替え)→ コミットは増えず、名札や HEAD が動くだけ

枝分かれも、マージも、fast-forward も、特別な魔法ではありません。「コミット=中身が変わらないスナップショット」「ブランチ=それを指す名札」「動くのは、いまいるブランチと HEAD だけ」── この単純なルールの積み重ねでできていたのですね。

この感覚を持ったまま、次の章へ進みましょう。


第2章 ファイルが居られる「5つの場所」

第1章では「履歴(コミット)」の話をしました。第2章では、もう一方の話、「ファイルは、いまどこにあるのか」を見ていきます。

Git を使うとき、1つのファイルは、次の5つの場所のどこかに(場合によっては複数に)存在しています。

場所どんな場所?
ワーキングディレクトリいま手で編集している作業フォルダです。エディタで開いているのは、ここです
ステージング(インデックス)「次のコミットに含めるもの」を選んで仮置きしておく場所です
ローカルリポジトリコミットの履歴を保管している本体です。.git フォルダの中で、第1章のオブジェクトが暮らしている場所です
リモート追跡リポジトリorigin/main など、リモートの状態の“写し(コピー)”です。実体は、あなたのPCの中にあります
リモートリポジトリGitHub などの、サーバー上にある本体です。5つの中で、唯一あなたのPCの外にあります

ここでぜひ覚えていただきたいのが、境界線です。**左の4つは、すべてあなたのPCの中(ローカル)**にあります。ですから、インターネットにつながっていなくても操作できます。サーバーと通信が必要になるのは、いちばん右の「リモートリポジトリ」とやり取りするときだけです。

入門者がつまずきやすいのが、origin/main の正体です。「リモートを追いかけるためにローカルにあるもの」と「本当のリモート」は別物なのです。origin/main リモートの状態をローカルに写しとったコピーで、実体はあなたのPCの中にあります。Git がオフラインでも動けるのは、「最後に通信したときのリモートの状態」を、この写しとして持っているからです。
ちなみに origin というのは、リモートの場所(URL)につけたあだ名にすぎません。GitHub 側は、自分が origin と呼ばれているなんて知りません。git remote -v というコマンドで「あだ名 → URL」の対応を確認できます。

1つのコマンドを追ってみる ── git commit でファイルはどこへ動く?

5つの場所が頭に入ったら、1つのコマンドを取り上げて、ファイルがどう動いていくかを具体的に追ってみましょう。ここでは、いちばん基本的な git commit を主役にします。

E.txt を編集してからコミットするまでを、「場所の移動」として描くと、次のようになります。

  1. ワーキングディレクトリ E.txt を編集します。この時点では、Git はまだ何も記録していません。「変更があるよ」という状態です。
  2. git add E.txt を実行すると、その変更がステージングへ移ります。「次のコミットに、これを含めてくださいね」という予約のようなものです。
  3. git commit を実行すると、ステージングの内容がローカルリポジトリに書き込まれ、**新しいコミット(=第1章のスナップショット)**が記録されます。ブランチの名札も、その新しいコミットへ進みます。

ポイントは、commit がやっているのは、結局**「ステージングの内容を、ローカルリポジトリの新しいコミットとして書き込む」ことだけ**だ、という点です。第1章で見た「スナップショットを作って、main を前に進める」が、まさにこれです。第1章(履歴)と第2章(場所)が、commit という1点でつながっているのですね。

残りの場所へ ── push / fetch / merge

commit は、ローカルリポジトリまでしか届きません。リモート(サーバー)側の場所まで含めると、1つのファイルの旅は、次のように完成します。

  • git add … ワーキング → ステージング
  • git commit … ステージング → ローカル
  • git push … ローカル → リモート(あわせて origin/main の写しも更新します)
  • git fetch … リモート → リモート追跡(origin/main を最新の状態にします)
  • git merge origin/main … リモート追跡 → ワーキング/ローカルへ取り込みます

ここで注目していただきたいのは、どのコマンドも「隣の場所へ運ぶ」という1区間の移動でしかないことです。そして、「ローカル ↔ リモート」の境界を越えて通信するのは push  fetch だけです。それ以外は、すべてあなたのPCの中で完結しています。

よく使う git pull は、じつは git fetch + git merge をまとめて実行するコマンドです。fetch でリモートの最新を origin/main(写し)に取り込み、merge でそれを今いるブランチに合流させています。
ここで意外と大事なのが、git merge origin/main ローカルの中だけで完結する処理だということです。origin/main は名前こそリモートっぽいのですが、実体はローカルにある写しなので、merge は通信せずにPCの中で終わります。通信しているのは fetch の瞬間だけ ── この役割分担が分かると、pull が「よく分からない便利コマンド」ではなくなります。

origin とは ── リモートにつけた「あだ名」と、フォーク開発の例

第2章のはじめに少し触れたとおり、origin は特別なものではなく、リモートリポジトリ(の URL)につけた“あだ名”にすぎません。git clone したときに Git が自動で付ける、デフォルトのあだ名が origin というだけです。そして、そのリモートの状態をローカルに写しとったものが、origin/main などのリモート追跡ブランチでした。

ここで大切なのは、リモートは1つに限らず、いくつでも登録できるということです。あだ名で区別するので、複数あっても迷いません。その典型例が、OSS(オープンソース)開発でよく使う「フォーク」です。

OSS に貢献するときは、こんな流れになります。

  1. 本家の OSS リポジトリを、自分の GitHub アカウントに**フォーク(コピー)**します。これが、あなた専用のコピーです。
  2. その自分のフォークを git clone します。すると、フォークが origin というあだ名で登録されます(あなたが push できるリモート)。
  3. さらに、フォーク元の本家を upstream(上流)というあだ名で追加します(git remote add upstream <本家のURL>)。

これで、1つのローカルリポジトリに、2つのリモートのあだ名が登録された状態になります。git remote -v で確認すると、次のように見えます。

origin    https://github.com/you/project.git       (あなたのフォーク)
upstream  https://github.com/original/project.git  (本家 OSS)

あとは、2つのあだ名を使い分けます。

  • 本家の最新を取り込むgit fetch upstream で本家の更新を upstream/main(写し)に取得し、自分のブランチに取り込みます。
  • 自分の変更を送るgit push origin で、自分のフォーク(origin)へ送ります。
  • 本家へ提案する:本家には直接 push できないのがふつうなので、フォークから**プルリクエスト(PR)**で「この変更を取り込みませんか?」と提案します。

ポイントは、origin  upstream ただのあだ名で、実体はどちらも「URL を指す名札」だということです。だからこそ、1つのローカルが両方からファイルを取り込み、送り先を選んで送り出せるのです。


第3章 コマンドごとに見る ── 「場所」と「コミット」の動きで理解する

第1章と第2章で、Git を見るための2つの目線が手に入りました。

  • ① ファイルの場所:ワーキング → ステージング → ローカル → リモート(第2章)
  • ② コミット(履歴)の動きHEAD やブランチがどう動くか(第1章)

ここからは、代表的なコマンドを1つずつ取り上げ、実行すると ① 場所と ② コミットがそれぞれどう動くのかを、図で見ていきます。どのコマンドも、結局はこの2つを動かしているだけです。手元の基本(add → commit)から、送る・取り込む(push → fetch → pull → merge)、移動・取り消し(switch → reset → clean)、そして応用(rebase)の順に見ていきましょう。

git add

git add は、ワーキングで編集したファイルを、ステージング(次のコミットの下書き)に登録するコマンドです。

① ファイルの場所はどう動く? 編集して中身が変わった E.txt(”Hi!”)が、ワーキングからステージングへコピーされます。ローカル(過去のコミット)は、まだ古い “Hello” のままです。

② コミット(履歴)はどう動く? 何も起きません。add はあくまで「次のコミットに含める準備」なので、履歴は1ミリも動きません。

add は「コミットに含めるものを選ぶ」操作です。場所だけを動かし、履歴は動かしません。

git commit

git commit は、ステージングに用意しておいた内容を、新しいコミットとしてローカルリポジトリに記録するコマンドです。

① ファイルの場所はどう動く? ステージングの E.txt(”Hello”)が、中身そのままでローカルリポジトリに記録されます。ワーキングやリモートは動きません。

② コミット(履歴)はどう動く? 新しいコミット B が作られ(親は A)、いまいるブランチ main がその B へ進みます。HEAD も一緒に動きます。

commit は「ステージングの内容を記録し、新しいコミットを作って、いまいるブランチを1つ進める」。第1章と第2章で見たことが、1つのコマンドの中で同時に起きています。

git push

git push は、ローカルの新しいコミットを、リモート(サーバー)へ送るコマンドです。

① ファイルの場所はどう動く? ローカルのコミット内容がリモートへ届きます。あわせて、手元の origin/main(リモートの写し)も最新に更新されます。

② コミット(履歴)はどう動く? リモート側のブランチが進み、それを写した origin/main  main に追いつきます(A → B)。

push は「ローカルからリモートへ送り、リモートのブランチを進める」。ネット通信して外へ出す、数少ないコマンドの1つです。

git fetch

git fetch は、リモートの最新を、ローカルの「写し」(origin/main)に取得するコマンドです。作業ブランチはまだ動きません。

① ファイルの場所はどう動く? リモートの新しい内容(”v2″)が、リモート追跡(写し)へ取り込まれます。手元の作業フォルダは、まだ変わりません。

② コミット(履歴)はどう動く? origin/main だけが新しいコミット C へ進みます。自分の main  B のまま据え置きです。

fetch は「取ってくるだけ」。手元の作業に反映するには、このあと merge が必要です。だからこそ、安全に最新だけを確認できます。

git pull

git pull は、リモートの最新を取得して、いまの作業ブランチに取り込むコマンドです。中身は git fetch + git merge の2段階をまとめたものです。

① ファイルの場所はどう動く? fetch で写しを取り、merge でローカルとワーキングまで反映します。結果、手元のファイルが最新(”v2″)になります。

② コミット(履歴)はどう動く? 写しを取り込んで、作業ブランチ main が最新の C へ進みます。

pull は「fetch して merge する」だけ。第2章で見た2つを、1コマンドにまとめた便利コマンドです。

git merge

git merge は、別のブランチを、いまの作業ブランチに合流させるコマンドです。

① ファイルの場所はどう動く? 合流後の内容が、ローカル(新しいマージコミット)とワーキングの両方に反映されます。

② コミット(履歴)はどう動く? 枝分かれした2つの先端(main = B と feature = C)を合流させるため、親を2つ持つマージコミット M ができ、main  M へ進みます。

merge も結局「コミットを作って、いまいるブランチを進める」。ただし、親が2つある点だけが特別です。

git switch / checkout

git switch(古い書き方では git checkout)は、別のブランチに移動するコマンドです。HEAD を移し、ワーキングをそのブランチの内容に書き換えます。

① ファイルの場所はどう動く? ワーキングの中身が、移動先のブランチ(develop)の状態に置き換わります。

② コミット(履歴)はどう動く? コミットは増えません。HEAD  main から develop へ移るだけです。

「移動」とは、HEAD を動かし、ワーキングをその場所の内容に合わせること。新しいコミットは作りません。(昔の checkout は「移動」と「ファイルの復元」の2役を兼ねて紛らわしかったため、いまは switch  restore に分かれました。)

git reset は、ブランチと HEAD を、指定した過去のコミットへ戻すコマンドです。--soft / --mixed / --hard の3つのモードがあり、「② コミット(履歴)を戻す」のはどれも共通で、「① どこまでファイルを巻き込んで戻すか」だけが違います。3つを別々のコマンドとして見ていきましょう。

git reset –soft

ブランチと HEAD だけを前のコミットへ戻します。ステージングとワーキングの中身は、そのまま残ります。

① ファイルの場所はどう動く? 動かしません。編集した "Hello2" は、ステージングにもワーキングにも残ったままです。

② コミット(履歴)はどう動く? main  HEAD が前のコミット A へ戻ります。取り残された B は「宙ぶらりん」状態になります(しばらくは復元できます)。

「コミットだけ取り消したい(変更は全部残したい)」ときに使います。

git reset –mixed(既定)

--soft に加えて、ステージングも前のコミットの状態へ戻します。ワーキングの変更は残ります。オプションを付けないときは、これになります。

① ファイルの場所はどう動く? ステージングだけが A の状態("Hello")に戻ります。ワーキングの編集 "Hello2" は残ります。

② コミット(履歴)はどう動く? --soft と同じく、main  HEAD  A へ戻ります。

「いったん add も取り消して、もう一度ステージングし直したい」ときに使います。

git reset –hard

ブランチ・HEAD・ステージング・ワーキングを、すべて前のコミットの状態へ戻します。

① ファイルの場所はどう動く? ステージングもワーキングも A に戻ります。編集した "Hello2" は消えてしまうので、3つの中で唯一、取り扱いに注意が必要なモードです。

② コミット(履歴)はどう動く? こちらも main  HEAD  A へ戻ります。

3つのモードに共通するのは「ブランチと HEAD を前のコミットへ動かす」こと。違いは「ファイルをどこまで道連れにするか」だけ、と覚えておけば取り違えません。

git clean

git clean は、Git が管理していない(追跡されていない)ファイルを、ワーキングから削除するコマンドです。

ここで「未追跡(untracked)」という言葉を説明しておきます。未追跡とは、一度も git add されていないファイルのことです。新しく作ったメモやビルドの生成物など、Git にまだ一度も登録していないファイルが、これにあたります。逆に、一度でも git add(やコミット)したファイルは「追跡されている(tracked)」状態になります。

① ファイルの場所はどう動く? 未追跡ファイル(temp.txt)だけが、ワーキングから消えます。一度でも追跡されたファイル(E.txt)は守られます。

② コミット(履歴)はどう動く? 何も起きません。

clean が消すのは「Git が一度も登録していない(未追跡の)ファイル」だけ。一度でも git add したファイルは Git が守ってくれます。

git rebase

git rebase は、自分のコミットを、別の土台(ブランチの先端)の上に作り直して積み直すコマンドです。

① ファイルの場所はどう動く? 積み直したあとの状態が、ローカルとワーキングに反映されます。

② コミット(履歴)はどう動く? feature のコミット CD が、main の先端 B の上に C′D′ として作り直されます。中身が同じでも、**新しいコミット(別のハッシュ)**になります。元の CD はどこからも指されなくなり、やがて消えます。

第1章の「オブジェクトは不変。変更は“作り直して差し替え”でしか起きない」が、いちばんはっきり現れるのが rebase です。だからこそ、共有済みのブランチで使うと事故のもとになります。

これで主要なコマンドを一巡しました。どれも結局、① ファイルの場所と **② コミット(履歴)**の組み合わせでしかなかったことが、図で確かめられたと思います。


まとめ

いかがでしたでしょうか?この記事がGitを理解するためのお役に立てれば幸いです。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。
エンジニア募集中!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です