リモートリポジトリの操作

はじめに

Gitをローカルだけで使うことはほとんど無く、リモートリポジトリを設定してそこと連携して使うことになるだろう。リモートリポジトリとしては、GitHubやGitLabといったホスティングサービスを使うのが一般的だ。以下では、主にリモートリポジトリのサーバとしてGitHubを想定し、リモートリポジトリとの操作について説明する。

リモートリポジトリとは

remote.png

複数の人が同じプロジェクトに所属して開発を進めている時、もしくは個人開発で家のマシンと大学のマシンの両方で開発を進めている時、複数の場所からプロジェクトの最新情報にアクセスできる必要がある。そのような時に使うのがリモートリポジトリだ。この時、リモートリポジトリに負わせる役目には二通りの考え方がある。一つは中央集権型で、履歴など情報を全てリモートリポジトリにのみ保存し、ローカルにはワーキングツリーのみ展開する、というものだ。もう一つは分散型で、リモートにもローカルにも全ての情報を保存しておき、適宜同期させるという方針を取る。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) という。

cycle

次にローカルで作業をする際、リモートの情報が更新されているかもしれないので、その情報をローカルに反映する。この作業を フェッチ (fetch) という。フェッチによりリモートの情報がローカルに落ちてくるが、ローカルの歴史は修正されない。ローカルの歴史にリモートの修正を反映するにはマージする。リモートの修正をローカルに取り込んだらローカルを修正し、作業が終了したらプッシュによりローカルの修正をリモートに取り込む。以上のサイクルを繰り返すことで開発が進んでいく。以下、それぞれのプロセスを詳しく見てみよう。

クローン

リモートリポジトリの情報をクローンする時、すなわち、ローカルに初めて持ってくる時にはgit cloneを使う。この際、クローン元の場所を指定する必要がある。GitHubのリポジトリをローカルにクローンする際には、通信プロトコルをHTTPSとするかSSHとするかにより、URLが異なる。例えばGitHubのappi-githubというアカウント(正確にはOrganization)の、clone-sampleというプロジェクトにアクセスしたい時、それぞれURLは以下のようになる。

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でクローンするためには、公開鍵による認証が必要となる。とりあえず

と覚えておけば良い。

クローンにより、それまでの「歴史」全てと、デフォルトブランチの最新のコミットがワーキングツリーとして展開される。

clone

以後は、ローカルリポジトリとして通常通りブランチを作ったり、コミットしたりすることができる。

プッシュ

ローカルで作業を行い、歴史がリモートよりも進んだとしよう。ローカルの歴史をリモートに反映することをプッシュと呼び、git pushにより行う。

push

フェッチ

ローカルにクローン済みのリポジトリがあり、リモートで歴史が進んでいる場合、その歴史をローカルに反映させる必要がある。その時に行うのがフェッチでありgit fetchにより行う。

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とすることが多い。

fetch_merge

リモートで更新された歴史をフェッチする前は、ローカルリポジトリはリモートが更新されていることを知らないので、mainorigin/mainは同じコミットを指している。しかしgit fetchによりリモートの情報が更新されると、新たに増えたコミットを取り込むと同時に、リモートのmainブランチが指しているコミットを、ローカルのorigin/mainブランチが指す。これにより、リモートの情報がローカルに落ちてきたことになる。

あとは、origin/mainを通常のブランチと同様にgit mergeすることで、リモートの修正をローカルのブランチに取り込むことができる。図ではfast-forward可能な状態であったが、歴史が分岐していた場合でも、ローカルの場合と同様にマージすれば良い。

上流ブランチとリモート追跡ブランチ

Gitではローカルにリモートの情報のコピーを用意しておき、それを介してリモートとやりとりする。慣れないとこのやりとりがイメージしづらいので、一度しっかり理解しておきたい。リモートとのやりとりには、特別なブランチを用いる。

いま、リモート(origin)にも、ローカルにもmainというブランチがあるとしよう。Gitでは、リモートにある情報も全てローカルにコピーがある。リモートoriginmainブランチに対応するブランチはorigin/mainという名前でローカルに保存されている。このブランチを、ローカルのmainブランチの 上流ブランチ(upstream branch) と呼ぶ。最初にクローンした直後、mainブランチと共に、「リモートのmainブランチ」に対応するorigin/mainというブランチが作成され、自動的にorigin/mainブランチがmainブランチの上流ブランチとして登録される。ローカルのorigin/mainは、リモートのmainを追跡しており、git fetchgit pushにより同期する。リモートのmainブランチに対して、origin/mainリモート追跡ブランチ (remote-tracking branch) と呼ぶ。図解すると以下のようになる。

upstream branch

ローカルのmainブランチにとっての「上流」はローカルのorigin/mainブランチであり、origin/mainmainの上流ブランチと呼ぶ。また、ローカルのorigin/mainブランチはリモートのmainブランチをリモート追跡しており、origin/mainをリモートのmainブランチのリモート追跡ブランチと呼ぶ。つまりorigin/mainは上流ブランチでもリモート追跡ブランチでもあることに注意したい。

最初にリポジトリをクローンした時、メインブランチであるmainができるが、自動的に上流ブランチorigin/mainも作成される。ローカルのmainはローカルのorigin/mainを、ローカルのorigin/mainはリモートのmainを見ている。

上流ブランチは、git fetchgit mergegit rebase等で、引数を省略した時の対象ブランチとなる。先のfetchmergepushなどの操作を、ブランチがどのように動くかも含めてもう一度見てみよう。

まず、リモートリポジトリのmainの歴史が、ローカルのmainよりも進んでいる状態でgit fetchしよう。mainに上流ブランチorigin/mainが設定されており、origin/mainはリモートのmainをリモート追跡しているため、これは

git fetch origin main

つまり「リモートリポジトリoriginmainブランチの指す情報をローカルとってこい」と同じ意味となる。するとリモートから「進んでいる歴史」分のコミットがローカルに落ちてきて、さらにローカルのorigin/mainブランチが先に進む。これにより、リモートのmainと、ローカルのorigin/mainが持つ歴史が同じになった。

fetch_remote.png

次に、git mergeを実行する。カレントブランチがmainであり、上流ブランチとしてorigin/mainが設定されているため、これは

git merge origin/main

と同じ意味となる。今回のケースではfast-forward可能であるため、単にmainorigin/mainの指すのと同じコミットを差すように移動する。これにより、ローカルのmainがリモートのmainと同じ歴史を持つようになった。

次にpushを見てみよう。コミットをすることで、ローカルにあるmainブランチの歴史が進んだ。しかし、origin/mainはそのままだ。この状態でgit pushをしよう。すると、

push remote

すると、ローカルで新たに追加されたコミットがリモートに送られ、リモートのmainブランチが先に進む。さらに、ローカルのorigin/mainブランチも先に進む。これにより、ローカルのmainブランチ、origin/mainブランチ、リモートのmainブランチが全て同じ歴史を共有できた。

まとめると以下のようになる。

実際にはgit fetchgit pushなどはリモートやブランチを自由に指定することができるが、それは必要になった時に覚えれば良い。まずはmainブランチのみをリモートと同期させ、git fetchgit pushは引数無しで実行するようにしておこう。

その他知っておいた方が良いこと

個人開発においては、リモート操作は初回のgit clone、そして開発中のgit fetchgit pushだけ覚えておけばよい。しかし、Gitには他にもリモート操作のためのコマンドがある。リモート操作がらみで気を付けるべきことと合わせて簡単に紹介しておこう。

git remote

リモートリポジトリを管理するコマンドが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が登録されており、fetchpushの対象となるURLとしてどちらもhttps://github.com/appi-github/clone-sample.gitが登録されている、という意味だ。なお、Gitは同じリモートリポジトリの名前でfetchpushに異なる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

これは

という操作を行う。もし-uオプションをつけなかった場合、

という処理のみ行い、mainブランチの上流ブランチの設定はしない。後でorigin/mainmainの上流にしたくなった場合はカレントブランチがmainの状態で

git branch -u origin/main

を実行する。これらの操作についてはGitHubの操作の項で改めて説明する。

git pull

git pullを実行すると、git fetchgit mergeを一度に行うことができる。カレントブランチに上流ブランチが設定されている状態で

git pull

を行うと、

git fetch
git merge

を実行したのと同じ状況になる。

しかし、git pullの動作は、特に引数を指定した時に直観的でないため、慣れない人が使うとトラブルを起こすことが多い。

慣れるまでは、とりあえずgit pullの存在は忘れ、git fetchしてから、git mergeする習慣をつければ良い。

プッシュしたブランチをリベースしない

リモートリポジトリとローカルリポジトリの「歴史」はgit fetchgit 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

これにより無理やりローカルの歴史をリモートに反映させることができる。

force push

しかし、push済みの歴史が改変されてしまうと、他のローカルリポジトリの持つ歴史と矛盾することになる。もともとリモートのmainc3というコミットを指していた。その状態でクローンしたリポジトリは、origin/mainc3を指すことになる。ところが、そのあとリベースにより改変された歴史が強制プッシュされてしまうと、origin/mainが指していたc3というコミットがなくなってしまう。

force push2

多人数開発であればもちろんの事、個人の開発でも、家と大学のPCでリポジトリの歴史に矛盾が出たら混乱することが予想できるであろう。

慣れるまでは、原則として

ということを守れば良い。もし「しまった!」と思った場合、ほとんどの場合、force pushする前であればなんとかなることが多いので、自分で解決しようとせず近くにいるGitに詳しい人に助けを求めること。

まとめ

Gitでは、リモートリポジトリとやりとりをすることで開発を進める。通常、リモートリポジトリは一つだけであり、originという名前を付ける。通常の開発の流れは以下のようになるだろう。

  1. git fetchによりリモートの更新をダウンロード
  2. git mergeによりリモートの更新を取り込む
  3. git switch -c newbranchにより新たにnewbranchブランチを切って作業開始
  4. 作業終了したら(好みに応じてnewbranchブランチからmainにリベースしてから)mainからnewbranchをマージする
  5. git pushする

リモートリポジトリとはリモート追跡ブランチを使って情報を同期する。ローカルにあるorigin/mainブランチは、リモートoriginmainブランチをリモート追跡するブランチであり、また多くの場合においてローカルのmainブランチの上流ブランチでもある。

Gitのリモートがらみには、git pullや、git push -fなど、危険なコマンドがある。意味を完全に理解するまではこれらのコマンドを使わないようにすると良いだろう。