この記事はNuco Advent Calendar 2022の7日目の記事です
はじめに
株式会社Nucoでエンジニアをしている@noshishiです。
今回は、ついついその場限りのコマンド実行で乗り越えがちなGitを、コマンドを使わず理解するための記事を書こうと思います。
Gitとは
バージョンを管理し、作業を分散する
Gitは、分散型バージョン管理システムと呼ばれるソースコードの管理システムの1種です。
Gitは、ファイルの変更履歴(バージョン)を記録・追跡することで、過去と現在のファイルを比較し、変更点を明らかにすることで、円滑に開発作業を進めるためのツールです。
また、一度に複数の開発者がファイルを編集できるシステムなので、作業を分散して行うことができます。
Gitを使うということ
まず、みんなで共有できる保存場所(以下、リモートリポジトリ)にあるファイルなどを、手元のパソコン(以下、ローカルリポジトリ)にコピーを作って、新しいコードやファイルを追加・編集します。
そして、ローカルリポジトリからリモートリポジトリへ登録することでファイルを更新していくことです。
![retool.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F22c6b2e3-aeda-44c6-8f54-3b7e80db129b.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=b64c132237473ff88854b6fc2a311cc5)
完全理解の鍵はイメージ
Gitを扱う上で、重要なのは 「何」から「何」へ・「どんな作業」を行う のかを追うことです。
コマンド操作だけだと、何が起きているかを理解できず、誤ったコマンドを入力する可能性があります。
(もちろん、著者も例外なくやっちまったことある勢です。)
Gitの操作は、操作前と操作後でどんなことが起こっているのかをイメージしよう。
一人で使うこともできるのでたくさん練習してみるのが良いです。
新しい作業を始める
リポジトリ
Gitにおけるリポジトリとは、ファイルを保存しておくための倉庫で、リモートとローカルの二つがあります。
リモートリポジトリは、ソースコードをインターネットのサーバーに置いて、みんなで共有できるリポジトリです。
ローカルリポジトリは、ソースコードを手元パソコンに置いて、自分だけが変更できるリポジトリです。
リポジトリを複製して作業開始
まず、自分の開発環境を用意します。
といっても、どのディレクトリで作業するかを決めるだけです。
例えば、ホームディレクトリでもいいし、普段使っているディレクトリで構いません。
次に、リモートリポジトリからファイルをコピーして持ってきます。
これをclone
といいます。
この段階で一緒に作業ディレクトリが作成されます。
なので、新しいディレクトリを作成しなくていいです。
![clone.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F36772bce-9111-1f50-bafd-97246035e78e.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=f31033d380bbf36c5305348ad3cb0ee3)
project
というリモートリポジトリには、first.txt
だけ入っていて、そのリモートリポジトリをclone
した時のイメージです。
もちろん最初にローカルリポジトリを作成して、その後にリモートに反映させることもあります。
これはinitialize
と呼ばれ、すでに作業しているディレクトリをリポジトリに変換することができます。
(補足)ワーキングディレクトリ
ワーキングディレクトリは、特殊なディレクトリではなく、いつも作業するディレクトリが、Gitによって拡張されるようになったと考えていただけたらと思います。
つまり、Gitが管理する対象のディレクトリ(今回であればproject
)は、Gitのステージングエリアやローカルリポジトリと接続できるということです。
![clone3.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F162c1db8-8271-fd4e-f702-2f77a0329564.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=04816a57f807d46f80bb3cd57bd9ce6a)
ファイルを変更・追加する
リポジトリのソースコードの変更は、ワーキングディレクトリ、ステージングエリアを通して行われます。
実際には、我々がソースコードを変更するのは、ワーキングディレクトリです。
新しく、second.txt
というファイルを作成してみます。
![create_file.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F8361cfea-fca9-3193-835b-e31b449df6b8.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=ab4776671324be27e5014db829b12490)
次に、ステージングエリアに変更したファイルを追加します。
これをadd
と言います。
変更したファイルをローカルリポジトリに反映させる前にワンクッションおくのがGitの特徴です。
なぜこのクッションがあるのか後ほど詳しく説明します。
![add.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fe975ecbf-c65f-f9ab-6dec-75984de363d6.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=3b49919714812f9abceb70e69e11796a)
そして、ステージングエリアで追加された内容をローカルリポジトリへ登録します。
これをcommit
と言います。
commit
するとステージングエリアは、空になります。
(※12/9追記)厳密には空ではありません。1
ちなみに、commit
する際にコメントがかけます。
今回なら、ファイルを追加したので、git commit -m 'add second.txt'
と書きましょう。
![commit.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Ff8908fd7-72b1-51d7-4aa6-c742b56a4fac.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=ad50e600a9dfca9fffc2c8906bf5e079)
コミットすると、リポジトリにコミットオブジェクトが作成されます。
コミットオブジェクトを超簡単に説明すると、更新者の情報や変更後のファイルが保存されているデータです。
(このときデータは、差分だけではなく、その瞬間のファイルの状態(スナップショット)全てが保存されます。)
Gitのオブジェクトについては、Gitのオブジェクトの中身を参考にしてください。
リモートリポジトリを変更する
上記作業で、自分の手元の作業は終わりました。
最後に行うのが、ローカルリポジトリの変更をリモートリポジトリへ反映させることです。
これをpush
と言います。
![push.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fd57789fe-08f2-3234-ec5a-bf1457a6cb6d.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=4246e8c564b928f689d2a73255861abf)
リモートリポジトリに対して行うcommitだと考えると分かりやすいかもしれません。
差分をみる
同じファイルの同士の変更点をdiff
といいます。
作業の途中で自分が行った変更を確認することができます。
git diff
というコマンドを使用します。
コマンドの詳しい説明は省きますが、よく使う3つを紹介します。
add
する前に、元のワーキングディレクトリとの変更点をみるgit diff
。
add
した後に、作業中のワーキングディレクトリとの変更点を見るならgit diff --stage
。
コミット同士を比較するならgit diff <commit> <commit>
など。
(余談)ステージングエリアというクッション
開発作業が大きくなると、多くの変更を一つのワーキングディレクトリで行うことあります。
全ての変更を一気にローカルレポジトリに登録するとどうなるでしょうか?
この場合、コミットを解析する時に、ある機能をどこで実装したかわからないといったことが起こりうります。
Gitでは、1つの機能につき一つのcommit
を行うことが推奨されています。
そのために、commit
を行う単位を細かく分けることができるステージングエリアがあるということです。
![push.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F5b25c70d-baf5-0913-d220-f60aedf583ea.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=fef302ab5fae9b548b7fab8c3c27a676)
必要な分だけステージングし、作業を進めたり、先にcommit
することで、実装ごとに履歴を辿れる効率的な開発を進めていこうというのがGitのコンセプトなのです。
まとめ
一度clone
して,作業ごとにadd
,commit
,push
が基本的な作業の流れです。
clone
:リモートリポジトリから自分の開発環境(ローカルリポジトリとワーキングディレクトリ)にコピーを作る。
add
:ワーキングディレクトリからステージングエリアにファイルを追加し、コミットの準備をする。
commit
:ステージングエリアからローカルリポジトリに登録する。この時、コミットオブジェクトが作成される。
push
:ローカルリポジトリからリモートリポジトリへ変更内容を登録する。
ブランチ
ファイルの変更や追加を複数の分岐で作業を行うためにbranch
を作ります。
main
ブランチで保存しているファイルは、現在進行形で使用されています。
ブランチを分ける理由は、現在稼働しているソースコードに影響を与えることなく作業を行うためです。
新しいブランチを作る
develop
というブランチを作ってみます。
git branch <new branch>
やgit checkout -b <new branch>
で作ることができます。
前者はブランチを作るだけ、後者はブランチを作ってそのブランチに移動します。
(ブランチはリポジトリ内で管理されています。)
![cretae_branch.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fa7f499f9-bf5e-0c58-1a35-59cd3e2a4c30.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=b654fefad27b993247d21c5199054d48)
ブランチを生やす時のポイントは、どのブランチを派生元にするかということです。
派生元をgit checkout -b <new branch> <from branch>
として指定することができます。
指定しなければ、現在作業しているブランチが<from branch>
になります。
ブランチは、実はコミット(厳密にいうとコミットオブジェクトのハッシュ値)のポインタです。
新しいブランチを生やすということは、派生元のブランチがポイントしているコミットを、新しいブランチも同様にポイントすることを意味します。
ブランチで作業を進める
作業するブランチを移動することをcheckout
すると言います。
現在作業しているブランチのポインタをHEAD
と呼びます。
つまり、main
ブランチからdevelop
ブランチ移動するというのはHEAD
を変えることを意味します。
![checkout_branch.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F3a7379b1-7a1c-35ba-02be-619d13a192ad.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=0896024d073e1a0d7ee13715042a8936)
現在は、Atr3ul
というコミットを両方のブランチが指しています。
先ほどはsecond.txt
をmain
ブランチでコミットして追加したので、f27baz
というコミットからひとつ前に進んでいる状態です。
ここから、develop
ブランチでsecond.txt
を変更し、新しいコミットを行うとします。
![in_branch.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F1ae0aa22-4c8d-411b-b20e-c29047dcdb4d.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=8fa1bc63a4a913898b4a50cd44094f8f)
そうすると図のように、m9sgle
という新しいコミットが作成され、develop
ブランチはそのコミットをポイントすることになりました。
現在のHEADの位置(作業ブランチの位置)やファイルがどの段階まで作業を進めたか、あるいは誰がその作業を行なっているかの状態をstatus
と言います。
コミットの矢印の理由について、オブジェクト指向の考え方に慣れている方だと分かるかもしれません。
これは「親」コミットと「子」コミットの関係を表しています。
親←-子
、つまり親(コミット)から生まれた子(コミット)がどれだけ成長(変化)したかというのが、前提としてあります。
(余談)Git-FlowとGitHub-Flow
ブランチの生やし方や運用は、開発チームごとによって異なると思います。
一方で、プログラミングの命名規則のように、Gitのブランチの生やし方には一般的なモデルが存在します。
簡単に2つを紹介します。こんなものがあるんだな程度でいいと思います。
「Git Flow」は、かなり複雑に入り組んだ構造をしています。
本来のあるべきGitの使い方みたいなモデルかなと思います。
![git_flow.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fa05bf11e-8ad0-e82f-e5f4-76498f4b5c46.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=7e2228ea5185523c51869b10ce051e7f)
各ブランチの定義
master
:プロダクトとしてリリースする用のブランチ。※このブランチ上での作業は行わない
develop
:開発用ブランチ。リリース準備ができたらreleaseへマージする。※このブランチ上での作業は行わない
feature
:機能の追加用。developから分岐し、developにマージする。
hotfix
:リリース後の緊急対応(クリティカルなバグフィックスなど)用。masterから分岐し、masterにマージすると共にdevelopにマージする。
release
:プロダクトリリースの準備用。リリース予定の機能やバグフィックスが反映された状態のdevelopから分岐する。
リリース準備が整ったら、masterにマージすると共にdevelopにマージする。
「GitHub Flow」は、Git Flowをやや簡略化したモデルです。
![github_flow.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fb717c9a0-7612-906f-2f10-3bf7a600e755.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=d79137d65ee176316715655a195ad4b3)
見ての通り、master
とfeature
だけで構成されており、主な違いとしてプルリクエスト(下のプルで説明)というクッションでブランチ間の統合を行います。
まとめ
基本的にmain(master)上で作業することはないので、行いたい作業単位でブランチを作成し、新しいコミットを作成していきましょう。
branch
:コミットに対する新しいポインタ
checkout
:HEAD
を移動させて、作業するbranch
を変える。
マージ
枝分かれたブランチ同士を統合することをmerge
と言います。
基本的に、main
ブランチやdevelop
ブランチに対して統合を行なっていきます。
注意点は、「どのブランチ」が「どのブランチ」を統合(吸収)するかの主語を間違わないことです。
必ず、派生元のブランチに(HEADを)移動して、派生先のブランチからの統合を行うことになります。
現在、feature
ブランチで作業を行なっていて、下記のような third.txt
を作成しました。
Hello, World! I'm noshishi, from Japan.
I like dancing on house music.
そして、add
してcommit
まで終えました。
![feature_commit.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F3fbb3116-4439-7753-74ec-260b043777f3.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=a3122764c968dad8ed1848e40f8cf409)
ファストフォワード
feature
ブランチが、派生元であるdevelop
ブランチから辿れるコミットを指しているとき、develop
ブランチはfast-forward
な状態と言います。
まずは、checkout
でdevelop
へ移動します。
![checkout_develop.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fd2564a3b-ab4f-32a3-edb4-5dffb38c7f89.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=612cfe7b5640e51ef17cac894153eba7)
この場合、develop
ブランチは全く進んでいないので、feature
ブランチをmerge
すると、単にコミットを前に進めるだけになります。
この時、develop
ブランチとfeature
ブランチは同じコミットを共有することになります。
![merge_feature_no_conflict.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F95d4195d-a9a5-1fec-710e-85887c2f7f1f.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=62de23d97e383f091a2836c8c9807bf2)
ノーファストフォワード
もし、develop
ブランチがコミットやマージによって、新しいコミットに進んでいたらどうなるでしょうか?
これをno fast-forward
な状態と言います。
develop
ブランチでは、first.txt
を変更を行なってcommit
まで終えました。
そのため、develop
ブランチと'feature
ブランチは、完全に枝分かれてしまいました。
![develop_commi.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fd97a8652-dd94-2835-4cf2-94eed01e6353.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=498b5a414d6cafc4805ca5a4054bacfc)
develop
ブランチから、feature
ブランチをmerge
しようとすると、Gitは変更履歴同士を確認します。
もしお互いに競合しあう編集をしていない場合は、すぐにmerge commit
が作成されます。
これをAutomatic merge
と呼ばれます。
![merge_feature_auto.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fa81e9577-6aec-e565-71bf-0836fd883974.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=8fac2d246dfcc26cbcf8c5eba7695b89)
コンフリクトに対処する
no fast-forward
状態で、作業内容が競合していることをconflict
と言います。
この場合は、手動でconflict
をしている内容を修正し、commit
を行います。
develop
ブランチでは、以下のようなthird.txt
が作成され、commit
されています。
Hello, World! I'm nope, from USA.
I like dancing on house music.
develop
ブランチでは、I'm nope, from USA
と書いてあり、
feature
ブランチでは、I'm noshishi, from Japan
と書いてあります。
1行目の内容が競合している状態です。
この時にmerge
を行うと、conflict
が起こります。
Gitがconflict
を解決してからcommit
してくれと指示が出てきます。
![conflict.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F794285e4-077a-17b1-4743-0e1f2685fba7.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=339e428f4dae9c279ecf44a636f83aed)
(作業場所は`develop`ブランチ)
指示通りにthird.txt
を見てみると、以下のような追記がされています。
<<<<<<< HEAD
Hello, World! I'm nope, from USA.
=======
Hello, World! I'm noshishi, from Japan.
>>>>>>> feature
I like dancing on house music.
=======
で区切られた上側のHEAD
がdevelop
ブランチの内容を表しています。
下側がfeature
ブランチを表しています。
まずどちらを採用するかを考え、今回はfeature
ブランチでの変更内容を採用することにしました。
その時の作業は、third.txt
を手作業で編集(不要な部分を削除)するだけです。
Hello, World! I'm noshishi, from Japan.
I like dancing on house music.
そして次に行うのが、add
してcommit
です。
conflict
が解消され、新しいmerge commit
が作成されます。
![hand_merge.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F5a20b54e-33bd-a973-e201-8bbdaba8c0cc.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=c0652034aadb90e7f7eac4c8289d27cb)
初心者が恐怖のコンフリクトですが、これを覚えればもう怖くありません。
merge
を行なってconflict
を解消したらなぜもう一度merge
しないのか?と疑問に思うと思います。
merge
を一度実行した時点でdevelop
ブランチはmerge
状態に入り、ブランチ同士の比較しconflict
がなければ、新しいファイルを自動でcommit
を行い、merge
状態が解消されます。
なので、conflict
を解消した後に、特別にcommit
しているのではないということです。
だからこそmerge commit
と呼ばれるわけです。
不要なブランチは削除する
統合されたブランチは、基本お役御免なので、削除していきます。
ブランチを放置しておくと、削除したいブランチから他のブランチに移動して、git branch -d <branch>
すればおさらばです。
ちなみに、削除されたらそのブランチのコミットは無くなるのかというとそうではありません。
マージしたブランチにしっかりと引き継がれています。
git log
を使用すると、ブランチ内で行なったすべてのコミットおよびマージしたブランチのコミットを閲覧できます。
(余談)ブランチの正体を知りたい
ブランチは、コミットを指すポインタと言いましたが、もうひとつ重要なデータを保持しています。
それは、そのブランチで行ってきたすべてのコミットです。
ブランチは、コミットの集合体であり、なおかつその中で最新のコミットを指すポインタを持っているということです。(厳密に言えば、ポインタしているコミットからそれ以前のコミット辿れるということです。)
図で表すと以下の通りです。
![branch_image.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F8cbb8c69-d048-778c-9d2d-db1f3be3b3be.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=4f1b140da40382cee82051b8ccdf6b61)
だから、Git Flowの様に横軸でブランチを考えることができるんです。
ちなみに、上の図を横軸にブランチを置いて書いてみるとこうなります。
![branch_image2.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F97ed7459-a4ee-afd9-1e2e-d7a0ffbf8f1e.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=00fcc100cf4e7c9a547ccc57c25cd916)
まとめ
no fast-forward merge with conflict
merge
:特定のブランチ(main
やdevelop
など)に、作業用のブランチ(feature
など)を統合(吸収)し、新しいコミットを作成すること。
リベース
各ブランチの派生元のコミットを変えてブランチ同士を統合することをrebase
と言います。
merge
に似ていますが、異なる点は、作業を行うブランチが 「派生先」ブランチということです。
develop
ブランチとfeature
ブランチで作業していているとしましょう。
![base_branch.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F0655a75b-4466-a069-0ad7-f98a2785996c.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=d54f230bea9d0ae2db9badb15b2b8b1c)
ブランチごとごっそり移動させる
develop
ブランチの現在のコミット、feature
ブランチに反映させるためには、feature
ブランチが派生したgp55sw
コミットから3x7oit
コミットに移動させる必要があります。
これはfeature
ブランチからgit rebase develop
とすることで一気に移動できます。
![move_branch.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Facecd79a-66eb-1836-f911-a76b2ad3dbef.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=e66dbc2b70e3964211d938bf22f822e4)
merge
をしているというより、develop
ブランチの最新のコミットからfeature
ブランチを生やし直すに近いです。
ただし、コミットごと移動し、新しいコミットを行うことが違いです。
![rebase_branch.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fd37b85ab-7b78-8a68-3302-6081edc036b1.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=dfcb389c6c5cf062e9172d43b69a080a)
なぜ、こんな移動(統合)をするのかというと、一つは fast-forward
になりいつでもmerge
を行いやすいことです。
もう一つは、コミットが一直線になることで容易にコミット履歴を辿れ、ファイルの更新順に整合性を持たせることができるためです。
リベースのコンフリクトに対処する
もちろんrebase
にもconflict
が存在します。
上記の場合、feature
ブランチでは、fourth.txt
を追加しましたが、その後develop
ブランチでのコミットでは、fourth.txt
に関わる変更がないため、conflict
は起こりません。
ですが、以下のように変更内容が被っていた場合、conflict
が起こります。
![rebase_conflict.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fc04e5c00-7523-ccfd-9311-421fe252fdc0.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=16b2e0169fa28ebd4f3a8a5165b03944)
でもmerge
と同じように対処すれば大丈夫です。
ただし、差分を確認してファイルの編集を終えたら、git rebase --continue
で作業を終わりましょう。
commit
しなくても自動でコミットしてくれます。
rebase
:ブランチの派生元であるコミットを移動させて新しいコミットを行うこと。
ローカルリポジトリを最新にする
ローカルである程度作業を進めると、リモートリポジトリが他の開発者によって更新されている場合があります。
この場合において、リモートリポジトリの情報を再度ローカルリポジトリに反映させるために行うのがpull
です。
ブランチとリポジトリ
ブランチは、各リポジトリに保存されています。
実際に作業を行うブランチです。
![brancha.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F76f507a4-232e-894e-e01f-328ab7138577.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=099d0788f7be2c4491ba9f8ded171c24)
一方で、ローカルリポジトリには、リモートリポジトリをコピーしたブランチがあります。
これは「リモート追跡ブランチ」と呼ばれます。
remotes/<remote branch>
でリモートのブランチと紐づく名前のブランチです。
これは、あくまでもリモートリポジトリを監視しているに過ぎません。
![remotes.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F9c014980-79e5-bba6-c3d8-c8f6a237178d.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=cbf292c123ab40ead8de4cd761b9a078)
最新の状況を確認する
リモートリポジトリのdevelop
ブランチがリモート追跡ブランチより一つ進んでいる状況だったとします。。
![pull_notupdate.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F3393d6c6-1a65-31c7-7afb-f98170f79872.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=542e4532671db90b38aab5e2402b95e7)
リモートリポジトリのブランチの最新の状況をリモート追跡ブランチに反映させることをfetch
と言います。
![fetch_update.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fff486c7c-963d-5962-0878-a144aab5893f.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=959f69e732ecb7786ce6f2f644382faa)
最新の状態に更新する
もう少し踏み込んで、ローカルブランチにも反映させたい場合、pull
を行います。
pull
するとまず、ローカルのリモート追跡ブランチが更新されます。
その後にローカルブランチにmerge
を行います。
![pull_update.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fa9526959-34d0-20fd-d128-4b5b37f19298.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=967ed9611896d86c19cf6d7968be72bb)
今回は、develop
ブランチの一つ先に進んだコミットがあったので、ローカルブランチのdevelop
ブランチにmerge
して新たなコミットが作成されました。
プルのコンフリクトに対処する
リモートリポジトリのコミットで行われた変更と、ローカルリポジトリのコミットで行った変更が競合してしまった時、pull
したときにリモート追跡ブランチとローカルブランチでconflict
が起こります。
下記の場合、remotes/develop
とdevelop
ブランチが競合しています。
![pull_conflict.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F7c719d9a-962d-7f27-9175-4ebb75857657.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=d6f957c372baa5f4c8a370d7dc8b7659)
pull
は、fetch
とmerge
なので、merge
のconflict
と同じ対処方法で解決できます。
今回はdevelop
がremotes/develop
をmerge
するので、作業ブランチはdevelop
です。
原因のフォルダを開いて、修正したらcommit
を行いましょう。
リモート追跡ブランチとmerge
って少し不思議に感じると思います。
というのも、基本的にローカルブランチと同じになっていることがほとんどだから直接現れてこないためです。
git log
でコミットを辿ると面白いことがわかります。
da7a... (HEAD -> develop, origin/develop)
実はリモート追跡ブランチは、ローカルブランチと並行しているブランチだったことがわかります。
(余談)プルリクエストの正体
基本的にリモートとローカルの関係は、リモートリポジトリからローカルリポジトリにpull
し、ローカルリポジトリからリモートリポジトリにpush
することになります。
ですが、GitHubをはじめとするサービスには、ローカルリモートリポジトリ内にあるブランチから、mainブランチのようなブランチにmerge
する前にrequest
を送るという仕組みをとっています。(※12/9追記)ローカルから直接反映する訳ではありません。
というのは、開発者の個人の判断main
ブランチなどにpush
して、リモートリポジトリを更新してしまうと誰もチェックできずに大きな障害が発生する可能性があります。
一旦上位の開発者がコードをレビューするプロセスを挟むのがpull request
です。
![pull_request.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F49ceb654-292c-0b89-f537-1a771be1a7cd.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=4e197d62469f7e438f006d6ab9bf9320)
pull
:fetch
+ merge
。pull
は、リモートリポジトリの状態をローカルリポジトリに反映させること。
ここでのmerge
は、ローカルブランチにリモート追跡ブランチを統合すること。
便利な機能
コミットを訂正する
指定した(あるいは直前の)コミットを訂正するためのcommit
をrevert
と言います。
例えばm9sgLe
でsecond.txt
をローカルリポジトリに追加したとしましょう。
revert
を行うと、新しいpgmx9s
というコミットを作成しsecond.txt
はローカルリポジトリの登録から削除します。
![revert.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F255417d6-a05f-b68c-89eb-436ec1b92c7c.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=c4efc352920ba33182c5cba3e4499b6e)
revert
の良さは、commit
を残せることです。後ほど紹介するreset
と区別しましょう。
作業内容を避難させる
変更ファイルがある状態で他のブランチに移動すると変更内容を保持したまま移動したり、あるいは移動できないことがあります。
そのため、commit
までするか変更を破棄してしまうかを選択しなければなりません。
そんな時に活躍するのがstash
です。
ワーキングディレクトリやステージングエリアにあるファイルを一時避難させることができます。
![stash.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fa8e83f88-bce5-560b-d496-80bea2918a65.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=c1bf5602c7c7ebb4231f141993991bb7)
他のブランチに移動したい時、stash
し、帰ってきたらstash pop
で避難したファイルを取り戻して作業を再開します。
特定のコミットを持ってくる
任意のコミットを現在のブランチに持ってきてコミットを作ることをcherry-pick
と言います。
まさにいいとこ取りのような機能です。
![cherry.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F27601d74-62c8-2814-dfc6-3e31e6abf246.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=36c0bac7502d342d9a9447d90b1c020f)
以前にfeature
ブランチのzvcd2e
コミットで実装した〇〇〇な機能だけ持ってきて、現在develop
ブランチの作業に使用したいときなどに使用します。
HEADを使いこなす
HEADは、現在作業中のブランチのポインタと説明しました。
また、ブランチはコミットを指すポインタだ、とも説明しました。
下の図を見てください。
![head.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2Fb4c0651f-a613-f858-9f53-7bf9de227f5d.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=8dad9a8310381b7476fdcaa558a03692)
HEADが指すものはdevelop
ブランチ、develop
ブランチが指すものeaPk76
というコミットです。
つまり、この状況でのHEADは、eaPk76
のコミットを指しているということになります。
よくGitのドキュメントや記事などに、コマンドの後ろにHEAD
を使うのを見たことがありませんか?
例えば、git revert HEAD
など。
これは、HEAD
からコミットを辿れるからこそ実現できるコマンドという分けです。
(おまけ))コミットを削除する
現在の最新のコミットを取り消してもう一度作業することをreset
といいます。
--soft
オプションを使用するとadd
した直後に戻ることができます。
--mixed
オプションを使用すると、ワーキングディレクトリで作業していた段階に戻ることができます。
--hard <commit>
オプションを使用すると、戻るコミット地点までのすべてのコミットを削除し、指定コミットにHEAD
を移動させます。
![reset.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F6d4a7eee-ce0a-4af5-ef0f-5541cb6b66b2.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=3faf4ff526e16251713efe626563bb11)
reset
はコミットログから完全にコミットが削除されるので、注意して使用しましょう。(12/9追記)2
もっとGitを知る
Git以外のソースコードの管理
Gitと同じ歴史を持ったMercurialというサービスがあります。
特徴は、Gitのような柔軟性を犠牲に非常にシンプルなコマンドラインインターフェース(CLI)を採用していることです。
最近だと、このMercurialをベースにMeta社がSaplingという新しいソースコード管理システムをオープンソースで公開されましたね。
また今度、ちょっと触ってみて感想を書いてみたいなと思います。
リモートリポジトリの居場所
リモートリポジトリ用のサーバーを貸してくれるサービスをホスティングサービスと言います。
代表的なものであれば、GitHub, Bitbucket、プライベートに使用するAws Code Commitなどがあります。
GitとGit Hubは、全く別物です。
ちなみに、上で書いた通り、リモートリポジトリ用のサーバーは自分達のサーバーでも大丈夫です。
ポインタ
C言語のようなメモリを直接扱うプログラミングに触れたことがある方は、なんとなく「ポインタ」の意味がわかると思います。
一方で、初学者の方にとって、すごく曖昧なものに感じると思います。
コミットオブジェクトは、リポジトリ内に保存されていると言いました。
リポジトリ内に、たくさんのコミットオブジェクトが溢れていたら、どのように欲しいオブジェクトを選ぶことができるでしょうか。
それは、特定のコミットオブジェクトありかを突き止めるラベル(住所)が必要になります。
![pointer.png](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F2918231%2F1d4f5378-f935-7703-b5bb-93ac76b73b28.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=a3b14f87fe2b94042b38b68afb6afe26)
「ポインタ」は、そのラベルを忘れないように指差してくれる貴重なデータというわけです。
ちなみにラベルは、ハッシュ関数と呼ばれる関数を通じて不思議な文字列へと変換されたものを使います。
気になる方は、Gitのハッシュ値の求め方を参考にしてください。
さらにGitを理解するために
この記事で言及できなかったことがたくさんあります。
- Gitのコアの部分はシンプルなキー・バリュー型データストア
- バリューとなるGitオブジェクトの詳細
- オブジェクトの扱い方
いつか完全攻略したいと思います。
お礼
最後まで読んでいただきありがとうございました。
この記事の作成を通して、本当の意味でGitと向き合えました。
まだまだ見習いエンジニアなので、なんとか小手先で解決したくなるGitのコマンドでしたが、最近ではイメージが湧いてくるのでコマンドが楽しくなってきました。誰かにとって同じような体験となれば幸いです。
GitHubでも公開しています。
GitHub上ではコマンドあり版も記載していく予定なので、もしよければご覧ください。
参考サイト
- Git Documentation
- Learn git concepts, not commands
- 図解 Git
- いまさらだけどGitを基本から分かりやすくまとめてみた
- git add ってなんのためにやるの?
最後に
弊社では、経験の有無を問わず、社員やインターン生の採用を行っています。
興味のある方はこちらをご覧ください。
-
※追記 ステージングエリアのファイル内容も、実はリポジトリに保存されています。ですが、説明のわかりやすさを優先し、空のボックスで表現しています。もしgitのリポジトリを詳しく見たい方は、プロジェクトのディレクト内にある、.gitフォルダを見てみてください!とても興味深いことがわかります。Git Documentation ↩
-
gitのリポジトリは、完全に削除することを基本的に許していないので、実はオブジェクトは残り続けています。ただし、
git log
では確認できないので、git reflog
で操作履歴から辿りましょう。もしreset
してしまったコミットを復活したい場合、有効な手立ては残されています! ↩