Gitをローカルだけで使うことはほとんど無く、リモートリポジトリを設定してそこと連携して使うことになるだろう。リモートリポジトリとしては、GitHubやGitLabといったホスティングサービスを使うのが一般的だ。以下では、主にリモートリポジトリのサーバとしてGitHubを想定し、リモートリポジトリとの操作について説明する。
複数の人が同じプロジェクトに所属して開発を進めている時、もしくは個人開発で家のマシンと大学のマシンの両方で開発を進めている時、複数の場所からプロジェクトの最新情報にアクセスできる必要がある。そのような時に使うのがリモートリポジトリだ。この時、リモートリポジトリに負わせる役目には二通りの考え方がある。一つは中央集権型で、履歴など情報を全てリモートリポジトリにのみ保存し、ローカルにはワーキングツリーのみ展開する、というものだ。もう一つは分散型で、リモートにもローカルにも全ての情報を保存しておき、適宜同期させるという方針を取る。Subversionなどが中央集権型であり、Gitは分散型である。分散型はそれぞれのリポジトリが完全な情報を保持していることから互いに対等なのだが、一般的には中央リポジトリという特別なリポジトリを作り、全ての情報を中央リポジトリ経由でアクセスする。この中央リポジトリを置く場所がGitHubである。
Gitでは、複数のリモートリポジトリを登録し、それぞれに名前をつけて管理することができる。しかし、通常はorigin
という名前のリモートリポジトリを一つだけ用意して運用することが多い。以下でもリモートリポジトリは一つだけとし、名前をorigin
とすることを前提とする。
Git管理下にあるプロジェクトには、ワーキングツリー、インデックス、リポジトリの三つの要素がある。ワーキングツリーは今作業中のファイル、インデックスは「いまコミットをしたら歴史に追加されるスナップショット」を表し、リポジトリはブランチやタグを含めた歴史を保存している。しかしリモートリポジトリはワーキングツリーやインデックスを管理する必要がない。そこで、歴史とタグ情報だけを管理するリポジトリとして ベアリポジトリ(bare repository) というものが用意されている。リモートリポジトリはこのベアリポジトリとなっている。ベアリポジトリはproject.git
と、「プロジェクト名+.git
」という名前にする。Gitの管理情報は、.git
というディレクトリに格納されているが、ベアリポジトリはその.git
の中身だけを含むリポジトリであることに由来する。git init
時に--bare
オプションをつけるとベアリポジトリを作ることができる。
git init --bare project.git
しかし、リモートサーバとしてGitHubを使うならば、ベアリポジトリを直接作成することはないであろう。ここでは、リモートリポジトリは「プロジェクト名+.git
」という名前にする、ということだけ覚えておけば良い。
ほとんどの場合、リモートリポジトリはネットワークの向こう側に用意する。したがって、なんらかの手段で通信し、かつ認証をしなければならない。まず、「リポジトリがインターネットのどこにあるか」を指定する必要がある。この、インターネット上の住所と言える文字列を Uniform Resource Locator (URL) と呼ぶ。例えばGoogle検索をする際、ブラウザでhttps://www.google.com/
にアクセスしているが、この文字列がURLである。
GitHubにアクセスする場合、通信手段(プロトコル)として大きく分けてSSHとHTTPSの二つが存在する。認証とは、「確かに自分がそこにアクセスする権限がある」ことを証明する手段であり、SSHでは公開鍵認証を、HTTPSでは個人アクセストークン(Personal Access Token, PAT)により認証をする。本講義ではSSHによる公開鍵認証を用いて、PATは用いない。SSH公開鍵認証については実習で触れる。
GitHubのリポジトリには、パブリックなリポジトリとプライベートなリポジトリがある。パブリックなリポジトリは、誰でも閲覧可能だが、プライベートなリポジトリは作者と、作者が許可した人(コラボレータ)しかアクセスできない。また、ローカルの修正をリモートに反映させるには適切な認証と権限が必要となる。
リモートリポジトリは、単にリモートと呼ぶことが多い。いま、自分が参加している、もしくは自分自身のプロジェクトのリポジトリがリモートにあったとしよう。最初に行うことは、リモートリポジトリからプロジェクトの情報を取ってくることだ。これを クローン(clone) と呼ぶ。クローンすると、リモートにある歴史の全てを取ってきた上で、デフォルトブランチ(main
)の最新のスナップショットをワーキングツリーとして展開する。このようにして手元のPCに作成されたリポジトリをローカルリポジトリ、もしくは単にローカルと呼ぼう。
さて、ローカルにリポジトリができたら、通常のリポジトリと同様に作業を行う。まずはブランチを切って作業をして、ある程度まとまったらメインブランチにマージする。これにより、メインの歴史がローカルで更新された。この歴史をリモートに反映することを プッシュ(push) という。
次にローカルで作業をする際、リモートの情報が更新されているかもしれないので、その情報をローカルに反映する。この作業を フェッチ (fetch) という。フェッチによりリモートの情報がローカルに落ちてくるが、ローカルの歴史は修正されない。ローカルの歴史にリモートの修正を反映するにはマージする。リモートの修正をローカルに取り込んだらローカルを修正し、作業が終了したらプッシュによりローカルの修正をリモートに取り込む。以上のサイクルを繰り返すことで開発が進んでいく。以下、それぞれのプロセスを詳しく見てみよう。
リモートリポジトリの情報をクローンする時、すなわち、ローカルに初めて持ってくる時にはgit clone
を使う。この際、クローン元の場所を指定する必要がある。GitHubのリポジトリをローカルにクローンする際には、通信プロトコルをHTTPSとするかSSHとするかにより、URLが異なる。例えばGitHubのappi-github
というアカウント(正確にはOrganization)の、clone-sample
というプロジェクトにアクセスしたい時、それぞれURLは以下のようになる。
- HTTPSの場合:
https://github.com/appi-github/clone-sample.git
- SSHの場合:
git@github.com:appi-github/clone-sample.git
git clone
によりリモートリポジトリをローカルにクローンするには、上記のURLを指定する。
まず、HTTPSプロトコルの場合は以下のように指定する。
git clone https://github.com/appi-github/clone-sample.git
すると、カレントディレクトリにclone-sample
というディレクトリが作成され、そこにワーキングツリーが展開される。リポジトリがパブリックである場合、誰でもHTTPSプロトコルを用いてクローンすることができる。ただし、ローカルの修正をリモートに反映させる(プッシュする)ためには、個人アクセストークンが必要だ。
SSHプロトコルの場合は以下のようにする。
git clone git@github.com:appi-github/clone-sample.git
パブリックなリポジトリである場合でも、SSHでクローンするためには、公開鍵による認証が必要となる。とりあえず
- 他人が作ったリポジトリを使うためにクローンする場合はHTTPS
- 自分が作ったリポジトリ(もしくはforkしたリポジトリ)を使うためにクローンする場合はSSH
と覚えておけば良い。
クローンにより、それまでの「歴史」全てと、デフォルトブランチの最新のコミットがワーキングツリーとして展開される。
以後は、ローカルリポジトリとして通常通りブランチを作ったり、コミットしたりすることができる。
ローカルで作業を行い、歴史がリモートよりも進んだとしよう。ローカルの歴史をリモートに反映することをプッシュと呼び、git push
により行う。
ローカルにクローン済みのリポジトリがあり、リモートで歴史が進んでいる場合、その歴史をローカルに反映させる必要がある。その時に行うのがフェッチでありgit fetch
により行う。
ここで注意したいのは、git fetch
は更新された歴史をローカルに持ってきてくれるが、ローカルのブランチは移動しない、ということだ。
実は、リモートの歴史を取ってくる際、リモートにあるmain
ブランチは、origin/main
という名前でローカルに保存される。リモートブランチはgit branch
では表示されないが、git branch -a
と、-a
オプションを付けると表示される。
$ git branch
* main
$ git branch -a
* main
remotes/origin/HEAD -> origin/main
remotes/origin/main
この時、remotes/origin/main
というのは、origin
という名前のリモートリポジトリのmain
ブランチであることを表現している。リモートリポジトリは複数設定することができ、それぞれに自由に名前をつけることができるが、通常はリモートリポジトリは一つだけ設定し、名前をorigin
とすることが多い。
リモートで更新された歴史をフェッチする前は、ローカルリポジトリはリモートが更新されていることを知らないので、main
とorigin/main
は同じコミットを指している。しかしgit fetch
によりリモートの情報が更新されると、新たに増えたコミットを取り込むと同時に、リモートのmain
ブランチが指しているコミットを、ローカルのorigin/main
ブランチが指す。これにより、リモートの情報がローカルに落ちてきたことになる。
あとは、origin/main
を通常のブランチと同様にgit merge
することで、リモートの修正をローカルのブランチに取り込むことができる。図ではfast-forward可能な状態であったが、歴史が分岐していた場合でも、ローカルの場合と同様にマージすれば良い。
Gitではローカルにリモートの情報のコピーを用意しておき、それを介してリモートとやりとりする。慣れないとこのやりとりがイメージしづらいので、一度しっかり理解しておきたい。リモートとのやりとりには、特別なブランチを用いる。
いま、リモート(origin
)にも、ローカルにもmain
というブランチがあるとしよう。Gitでは、リモートにある情報も全てローカルにコピーがある。リモートorigin
のmain
ブランチに対応するブランチはorigin/main
という名前でローカルに保存されている。このブランチを、ローカルのmain
ブランチの 上流ブランチ(upstream branch) と呼ぶ。最初にクローンした直後、main
ブランチと共に、「リモートのmain
ブランチ」に対応するorigin/main
というブランチが作成され、自動的にorigin/main
ブランチがmain
ブランチの上流ブランチとして登録される。ローカルのorigin/main
は、リモートのmain
を追跡しており、git fetch
やgit push
により同期する。リモートのmain
ブランチに対して、origin/main
を リモート追跡ブランチ (remote-tracking branch) と呼ぶ。図解すると以下のようになる。
ローカルのmain
ブランチにとっての「上流」はローカルのorigin/main
ブランチであり、origin/main
をmain
の上流ブランチと呼ぶ。また、ローカルのorigin/main
ブランチはリモートのmain
ブランチをリモート追跡しており、origin/main
をリモートのmain
ブランチのリモート追跡ブランチと呼ぶ。つまりorigin/main
は上流ブランチでもリモート追跡ブランチでもあることに注意したい。
最初にリポジトリをクローンした時、メインブランチであるmain
ができるが、自動的に上流ブランチorigin/main
も作成される。ローカルのmain
はローカルのorigin/main
を、ローカルのorigin/main
はリモートのmain
を見ている。
上流ブランチは、git fetch
、git merge
、git rebase
等で、引数を省略した時の対象ブランチとなる。先のfetch
、merge
、push
などの操作を、ブランチがどのように動くかも含めてもう一度見てみよう。
まず、リモートリポジトリのmain
の歴史が、ローカルのmain
よりも進んでいる状態でgit fetch
しよう。main
に上流ブランチorigin/main
が設定されており、origin/main
はリモートのmain
をリモート追跡しているため、これは
git fetch origin main
つまり「リモートリポジトリorigin
のmain
ブランチの指す情報をローカルとってこい」と同じ意味となる。するとリモートから「進んでいる歴史」分のコミットがローカルに落ちてきて、さらにローカルのorigin/main
ブランチが先に進む。これにより、リモートのmain
と、ローカルのorigin/main
が持つ歴史が同じになった。
次に、git merge
を実行する。カレントブランチがmain
であり、上流ブランチとしてorigin/main
が設定されているため、これは
git merge origin/main
と同じ意味となる。今回のケースではfast-forward可能であるため、単にmain
がorigin/main
の指すのと同じコミットを差すように移動する。これにより、ローカルのmain
がリモートのmain
と同じ歴史を持つようになった。
次にpushを見てみよう。コミットをすることで、ローカルにあるmain
ブランチの歴史が進んだ。しかし、origin/main
はそのままだ。この状態でgit push
をしよう。すると、
すると、ローカルで新たに追加されたコミットがリモートに送られ、リモートのmain
ブランチが先に進む。さらに、ローカルのorigin/main
ブランチも先に進む。これにより、ローカルのmain
ブランチ、origin/main
ブランチ、リモートのmain
ブランチが全て同じ歴史を共有できた。
まとめると以下のようになる。
git fetch
により、リモートのmain
とローカルのorigin/main
が同じ状態になるgit merge
により、ローカルのmain
とorigin/main
が同じ状態になるgit push
により、ローカルのmain
とorigin/main
、リモートのmain
が同じ状態になる
実際にはgit fetch
やgit push
などはリモートやブランチを自由に指定することができるが、それは必要になった時に覚えれば良い。まずはmain
ブランチのみをリモートと同期させ、git fetch
やgit push
は引数無しで実行するようにしておこう。
個人開発においては、リモート操作は初回のgit clone
、そして開発中のgit fetch
とgit push
だけ覚えておけばよい。しかし、Gitには他にもリモート操作のためのコマンドがある。リモート操作がらみで気を付けるべきことと合わせて簡単に紹介しておこう。
リモートリポジトリを管理するコマンドがgit remote
だ。例えばgit remote -v
で、リモートリポジトリのURL等を知ることができる。
適当なリポジトリをHTTPSでクローンしてみよう。
git clone https://github.com/appi-github/clone-sample.git
clone-sample
というディレクトリができたはずなので、そこに入ってgit remote -v
を実行してみよう。
$ cd clone-sample
$ git remote -v
origin https://github.com/appi-github/clone-sample.git (fetch)
origin https://github.com/appi-github/clone-sample.git (push)
これは、リモートリポジトリの名前としてorigin
が登録されており、fetch
とpush
の対象となるURLとしてどちらもhttps://github.com/appi-github/clone-sample.git
が登録されている、という意味だ。なお、Gitは同じリモートリポジトリの名前でfetch
とpush
に異なるURLを指定できるが、本講義では扱わない。
もしSSHプロトコルでcloneしていた場合には以下のような表示となる。
$ git remote -v
origin git@github.com:appi-github/clone-sample.git (fetch)
origin git@github.com:appi-github/clone-sample.git (push)
git remote
を普段使うことはあまりないが、既存のローカルリポジトリをGitHubに登録する時には必要となる。その場合は、まずGitHubにベアリポジトリを作っておき、
git remote add origin git@github.com:アカウント名/project.git
などとしてリモートリポジトリをローカルリポジトリに登録する。また、ローカルのmain
ブランチに上流ブランチを設定する必要がある。git branch -u
で設定することもできるが、最初のgit push
時に-u
で指定するのが一般的だ。
git push -u origin main
これは
- リモートの
main
をリモート追跡するブランチorigin/main
ローカルブランチを作る - 情報をリモートに送信する
main
の上流ブランチとしてorigin/main
を設定する
という操作を行う。もし-u
オプションをつけなかった場合、
- リモートの
main
をリモート追跡するブランチorigin/main
ローカルブランチを作る - 情報をリモートに送信する
という処理のみ行い、main
ブランチの上流ブランチの設定はしない。後でorigin/main
をmain
の上流にしたくなった場合はカレントブランチがmain
の状態で
git branch -u origin/main
を実行する。これらの操作についてはGitHubの操作の項で改めて説明する。
git pull
を実行すると、git fetch
とgit merge
を一度に行うことができる。カレントブランチに上流ブランチが設定されている状態で
git pull
を行うと、
git fetch
git merge
を実行したのと同じ状況になる。
しかし、git pull
の動作は、特に引数を指定した時に直観的でないため、慣れない人が使うとトラブルを起こすことが多い。
慣れるまでは、とりあえずgit pull
の存在は忘れ、git fetch
してから、git merge
する習慣をつければ良い。
リモートリポジトリとローカルリポジトリの「歴史」はgit fetch
やgit push
により同期することができる。git fetch
をした場合、Gitはローカルのorigin/main
が指すコミットと、リモートのmain
が指すコミットを比較することで「差分」を検出する。したがって、git fetch
をする場合、ローカルのorigin/main
が指すコミットがリモートに存在することが前提となる。push
も同様だ。
普通に作業をしていれば、歴史は増える一方で減ることはないから、昔存在したコミットが消えることはなく、origin/main
が指すコミットは必ずリモートに存在することになる。しかし、Gitには歴史を改変できるコマンドがある。git rebase
だ。
git rebase
により歴史を改変すると、リモートとローカルで歴史が食い違ってしまう。すると、git push
は差分の追加だけ(fast forward)でリモートを更新することができなくなる。
このような時のために、git push
に-f
オプションをつけることで、無理やりpushする、force pushというオプションが用意されている。
git push -f
これにより無理やりローカルの歴史をリモートに反映させることができる。
しかし、push済みの歴史が改変されてしまうと、他のローカルリポジトリの持つ歴史と矛盾することになる。もともとリモートのmain
はc3
というコミットを指していた。その状態でクローンしたリポジトリは、origin/main
がc3
を指すことになる。ところが、そのあとリベースにより改変された歴史が強制プッシュされてしまうと、origin/main
が指していたc3
というコミットがなくなってしまう。
多人数開発であればもちろんの事、個人の開発でも、家と大学のPCでリポジトリの歴史に矛盾が出たら混乱することが予想できるであろう。
慣れるまでは、原則として
- プッシュ済みのブランチ(特に
main
やmaster
)はリベースしない(リベースはローカルブランチのみ) - force push (
git push -f
)は使わない
ということを守れば良い。もし「しまった!」と思った場合、ほとんどの場合、force pushする前であればなんとかなることが多いので、自分で解決しようとせず近くにいるGitに詳しい人に助けを求めること。
Gitでは、リモートリポジトリとやりとりをすることで開発を進める。通常、リモートリポジトリは一つだけであり、origin
という名前を付ける。通常の開発の流れは以下のようになるだろう。
git fetch
によりリモートの更新をダウンロードgit merge
によりリモートの更新を取り込むgit switch -c newbranch
により新たにnewbranch
ブランチを切って作業開始- 作業終了したら(好みに応じて
newbranch
ブランチからmain
にリベースしてから)main
からnewbranch
をマージする git push
する
リモートリポジトリとはリモート追跡ブランチを使って情報を同期する。ローカルにあるorigin/main
ブランチは、リモートorigin
のmain
ブランチをリモート追跡するブランチであり、また多くの場合においてローカルのmain
ブランチの上流ブランチでもある。
Gitのリモートがらみには、git pull
や、git push -f
など、危険なコマンドがある。意味を完全に理解するまではこれらのコマンドを使わないようにすると良いだろう。