Git
読み:ぎっと
外語:Git

 Linuxカーネルの開発用に開発された分散型バージョン管理システムLinus Torvaldsが開発し、濱野純にメンテナンスが引き継がれた。
目次

概要
 Linuxでは当初、Linusがお気に入りだったBitKeeperがバージョン管理システムに採用されていた。
 しかしこれは「商用ソフトウェア」という、フリーソフトウェア開発には向いていない問題があった。さらに、価格変更で無料バージョンの提供も終了となり、諸々のトラブルが顕著化、ついに代替となるGitの開発が始まった。
 結果として作られたGitはもちろん、Linuxだけではなく、FreeBSDはじめ他のOSでも使われている。

特徴

分散化
 Git最大の特徴は、分散型という点である。リポジトリが利用者それぞれに「分散」され、中央サーバーを必要としないことを特徴としており、これがSubversionCVSなど従来のものに対するアドバンテージとなっている。
 Gitは、「マスターリポジトリ」から各ユーザーの環境にリポジトリの複製を持たせるような構造となっている。このため、インターネットに繋がっていなくても、ローカルにあるリポジトリにコミットできる。つまり、電波が届かない地下鉄や飛行機の中、あるいは山奥にいても、必要に応じて変更をコミットできる。そして、マスターリポジトリへの反映は、ネットワーク接続が可能になってから可能で、それは「マージ」という。
 さらに利点として、単にコミットしただけでは手元のリポジトリのみに反映されるため、マスターリポジトリに影響しない、という点がある。リポジトリへの変更が適切であればマスターリポジトリに対して変更をマージできるが、必要がなければマージしなくても良い。一度複製されたリポジトリは、マージさえしなければ手元で好き放題できるということである。
 ただし、実際の運用ではマージは必須であること、マスターリポジトリが落ちている間はマージも何もできないことから、分散化自体が大きなアドバンテージと言えるかどうかは定かでは無い。実際、GitHubを用いた中央集権型のソース管理というのはありがちで、2016(平成28)年1月28日午前(日本時間)にGitHubが陥落したさい「仕事にならない」「帰りたい」などのツイートがTwitterに散乱していた。

ブランチ
 Gitでは、あらゆるものをブランチとして扱う。
 ブランチは枝分かれしたものという意味で、本流から枝分かれしたものがブランチであり、通常はこのブランチに対してアクセスすることになる。
 デフォルトのブランチは「master」という名前のブランチである。

タグ
 Gitにはタグ機能があり、歴史上の重要なポイントに印をつけることが可能。
 何らかのコミットにタグを付けて目印にしたい場合は、まずコミットを実施し、その次にタグ追加の作業をする。
 現在のコミットにタグを付ける場合は、次のいずれかをする。
 $ git tag <tag-name>
 $ git tag -a <tag-name> -m 'comment'
 $ git tag -s <tag-name> -m 'comment'
 -aも-sもないと軽量版のtagで、単なる目印のポインターでしかない。-aを付けると、commitの時と同様に注釈を付け、誰がtagを付けたかを記録できる。-sは更に厳密で、秘密鍵を持っていればtagに署名をすることができる。
 また-mは省略可能で、略せば指定されたエディターが起動する。これもcommitの時と同様である。
 存在するタグを確認する場合は、次のようにする。
 $ git tag
 git logにタグを表示するには、次のようにする。
 $ git log --decorate=full
 tagをキーとして見ることもでき、ハッシュの代わりにタグ名を書くだけである。注釈や署名があれば、それが最初に表示される。
 $ git show <tag-name>
 tagを削除するには、次のようにする。
 $ git tag -d <tag-name>
 このtagからbranchを作る場合は、次のようにする。
 $ git chekout -b <branch name> <tag-name>
 また、このchekout -bオプションは、次と同じ動作であり、次の二つの操作を実施しても同じ結果となる。
 $ git branch <branch name> <tag-name>
 $ git checkout <branch name>

コミット

ハッシュ
 リポジトリに変更点を登録する作業をコミットというが、Gitでは、コミットを160ビットのSHA-1のハッシュで管理している。
 このハッシュは、たとえば、git show <sha1> とすることで、特定のコミットの情報を表示するのに用いられる。またこれは160ビットぶん(40文字)全部を入れる必要は無く、先頭一致で検索されるため、先頭から4文字以上の適当な文字数だけでよい。
 ただし、先頭から一部の場合、2件以上に一致する場合には次のように表示され、より長い入力を求められる。
 short SHA1 XXXX is ambiguous.

コミットIDの短縮表示
 Gitの標準では、ハッシュの頭7文字が短縮表示される。
 仕様として、8000コミット以上になると頭7文字では一意に指定できない恐れが高まるとされている。実際、Linuxの開発ではGitを用いているが、既に頭7文字では被る事例が相次いでいるとのことである。
 被って何が問題なのかということだが、つまり、表示される7文字でgit showしようとしても、
 short SHA1 XXXXXXX is ambiguous
 となってしまい、そのコミット内容をどうやっても表示することができないということである。

衝突の頻度
 2n個のハッシュキーに対し、衝突確率を1/(2m)程度に抑えるためには、2(n+m)ビットのハッシュ値が必要である。
 Linuxのコミット数は2012(平成24)年頃で約22万回に達したとされる。これを収める最小のビット数は218(=262144)なので18ビット。衝突確率を1/2つまり1/(21)にするためには、2×(18+1)=38ビットが必要なことがわかる。1文字で4ビットなので10文字(40ビット)が最低必要となる。Linuxの場合、10文字でも衝突は生じているが、11文字まで増やせば劇的に衝突率は減るだろうことが予想される。
 では逆に、いまの7文字で可能な範囲はどの程度かを求めてみる。
 4ビット/文字×7文字=28ビットなので、このビット数で衝突確率が1/2になる数を求めると、(28ビット/2)-1=13ビット、213=8192となる。
 つまり、Gitの仕様として、7文字では約8000コミットを超えると半分の確率でハッシュキーが衝突し、使い物にならなくなる。

機構

階層
 Gitには、上位から順に次の三つの階層が存在する。
 git add すると、ワークツリーにあるものがインデックスに入る。しかしインデックスではイメージが湧きにくいため、インデックスと呼ぶ代わりに「ステージングエリア」と呼ぶこともある。
 そしてリポジトリは、前述の通り、手元と遠隔に存在しうる。手元のリポジトリをローカルリポジトリ、遠隔にあるリポジトリをリモートリポジトリという。
 実際に手元に存在し編集でき目に見えるファイルは、ワークツリーのファイルである。

HEAD
 リポジトリには、最古から最新まで歴史がある。そのうち、最新の状態を指し示すポインター(指標)を「HEAD」という。
 後にも述べるが、HEADを指定してcheckoutすれば、ファイルをリポジトリの状態に戻すことができる。
 HEADの移動には、後に詳細に述べるが「git reset」コマンドを使う。

各階層の動き

初期状態
 新規にリポジトリをチェックアウトしたばかり、あるいはコミットを終えてからまだ何も編集していない状態。
 この時は、ワークツリーとインデックスはHEADに一致している。
 
 ここで、図の一番上にXYZ…とあるのがリポジトリであるものとする(これは説明上の目印で、実際のリポジトリにこのような名前があるわけではない)。

ファイル更新
 初期状態に対し、ワークツリーのファイルを変更した場合。
 この時、ワークツリーの内容はHEADとは一致しなくなるが、コミットするまではインデックスの内容はHEADのまま変化しない。
 

git add
 git add <ファイル名>で、ワークツリーのファイルをインデックスに反映することができる。
 インデックス(索引)はリポジトリと違って目に見えにくいが、コミットするまでの一時エリアとして使われる。
 

更にファイル更新
 この状態で更にファイル更新してもよい。
 この場合、再びワークツリーの内容はHEADとは一致しなくなる。結果として、この状態はワークツリー、インデックス、HEADは全て異なるものを差している。
 後述するgit resetの説明はこの状態を元に説明するが、まずはコミットまでを順に説明する。
 

更にgit add
 変更されたファイルがインデックスに反映されることで、再びワークツリーとインデックスは一致する。
 

コミット
 コミットすると、リポジトリにインデックスの内容を登録し、HEADはその位置に移動する。
 これにて、再びワークツリーとインデックスはHEADに一致する。
 

git reset

元に戻す処理
 ワークツリー、インデックス、HEADが全て異なるものを差している状態で、何らかの元の状態に戻したいとする。
 HEADの移動には「git reset」コマンドを使う。このコマンドでは主に、--soft、--mixed、--hardの3オプションが代表として使われる(省略すると--mixedと同義)が、これはHEADと共に、インデックスとワークツリーをどうするかを決めるものである。
 

HEADの場合

git reset --soft HEAD
 HEADをHEADの位置に移動させ、インデックスとワークツリーはそのままにする。
 つまり何も起きない
 

git reset --mixed HEAD
 HEADとインデックスをHEADに変更する。ワークツリーは変化しない。
 更新前のインデックスは失われる。
 git addの取り消し方法としてよく使われている。
 

git reset --hard HEAD
 HEAD、インデックス、ワークツリーをHEADに変更する。
 更新前のインデックスとワークツリーは失われる。
 

HEAD^の場合
 HEADではなく、HEAD^もよく使われる。

git reset --soft HEAD^
 HEADの位置をHEAD^の位置に移動する。インデックスとワークツリーは変化しない。
 元のHEAD位置はORIG_HEADとして記録されるので、間違えて実行しても一回だけなら簡単に戻せる。
 

git reset --mixed HEAD^
 HEADとインデックスをHEAD^に変更する。ワークツリーは変化しない。
 更新前のインデックスは失われる。
 コミットをやり直す前提で、直前のgit commitを取り消す場合によく使われている。ワークツリーは元のまま残るので、再加工してからコミットできる。
 

git reset --hard HEAD^
 HEAD、インデックス、ワークツリーをHEAD^に変更する。
 更新前のインデックスとワークツリーは失われる。
 git commitの取り消し方法としてよく使われている。
 元のHEAD位置はORIG_HEADとして記録されるので、インデックスやワークツリーがORIG_HEADと一致していれば誤って実行しても簡単に戻せるが、さもなくば復旧困難または不可能なので注意。
 

諸々の操作

作業中のゴミを消したい
 ソースの修正→ビルド→動作確認、などを続けていると、いろいろなファイルに手が加わった結果、ブランチを切り替えたりする時に面倒なことになる。
 そこで、ワークツリーの状況を綺麗な(リポジトリの)状態に戻したい時には、次のようにする。
 $ git reset --hard HEAD
 または
 $ git checkout HEAD -- .
 特定のファイルだけ、編集や削除を戻すときは、ファイル名が <ファイル名> だとすると、
 $ git checkout HEAD -- <ファイル名>
 でだいたい戻る。

一旦git addしたものをやめたい
 一旦git add をしたが、やはり止めて別のコミットをしたい、と言った場合は次の方法でaddを取り消すことができる。
 $ git restore --staged <ファイル名>

過去のcommitを修正したい
 まず、変更中のファイルがあればcommitするか、別に避けておいてgit resetないしgit checkoutなどをして変更がない状態にしておく。
 次にgit logで、どのくらい過去であるかをあらかじめ確認しておく。修正すべきコミットが過去5件以内であれば、例えば次のようにしてコミットの修正を開始する。
 $ git rebase -i HEAD^^^^^
 Vimなど設定したエディターが開き戻した分までのコミットが出てくるため、編集したいコミットの頭の pick を edit に書き換える。Vimなら書き換えたあと、:wq などで書き込めば、歴史改竄の作業が始まる。
 ここで例えば、コミットをし忘れていた sample.txt を追加する場合を例とすると、次のようになる。
 $ git add sample.txt
 $ git commit --amend --no-edit
 これで当該のコミットに、sample.txtが追加される。
 あとは次をして、rebaseを継続させる。
 $ git rebase --continue
 先にeditに書き換えた全てのコミットの書き換えが終わると、修正の作業は終了となる。

別のbranchをpushしたい
 pushについては後述するが、具体的には次のようにする。pushしたい現在のブランチ名を nowbranch であるとする。
 $ git push --set-upstream origin nowbranch
 この方法でpush後は、特に何も指定しなくても、pushだけでnowbranchの内容がpushできるようになる。

リポジトリとの連携

手順

リポジトリ‖インデックス←ワークツリー
 ワークツリーの内容をインデックスに登録するには、次のようにする。
 $ git add <ファイル名>
 実際には様々なオプションがあり、無視ファイルの設定などもあるが、もっとも単純な方法はこの通りである。

リポジトリ←インデックス‖ワークツリー
 インデックスの内容をリポジトリに登録するには、次のようにする。
 $ git commit
 Gitにおけるコミットは、手元にあるリポジトリのみに反映される。
 コミットする際にコメントを残すことができる。通常はvi相当のエディタが起動するが、次の方法で、コマンドラインからコメントを指定することでエディタの起動を抑止できる。
 $ git commit -m "commit message"

リポジトリ‖インデックス→ワークツリー
 Gitにおける復活の呪文はcheckoutである。
 作業ファイルを、リポジトリの内容ではなくインデックスの状態に戻すには、次のようにする。
 $ git checkout <ファイル名 または ブランチ名>
 checkoutは、指定されたファイルをインデックスに記録されている状態に復帰させる。
 ファイル名(パス付きももちろん可)を書けばそのファイルを、ブランチ名を書けば全ファイルを、インデックスの状態に戻す。
 $ git checkout hoge.c

リポジトリ→→→→→→→→ワークツリー
 インデックスを飛び越え、リポジトリの内容をワークツリーに復帰するには、次のようにする。
 $ git checkout HEAD <ファイル名 または ブランチ名>
 Gitでは、「現時点での最新のコミット」をHEADという。
 HEADを指定してcheckoutすると、最新のコミットされた内容(最新のリポジトリ)にワークツリーを復帰させる。

ほかのリポジトリとの連携

pull/push
 ほかのリポジトリ、つまりリモートリポジトリの変更点をマージするには、次のようにする。
 $ git pull <repository name>
 逆に、リモートリポジトリに自分のリポジトリの内容を送信するするには、次のようにする。
 $ git push <repository name> <branch name>

テクニック
 多くの人が利用するリモートリポジトリに対してpull/pushする場合、手元でコミットを続けたものを満を持してpush、といった場合に確実にコンフリクト(衝突)を起こす。
 この解決方法は様々あるが、面倒なことが多い。
 一番手っ取り早いのは、適当なブランチを一つ作ってそこに対してコミットしておき、いざpushする時には、そのブランチをpush先のリポジトリへrebaseし、その後mergeしてからpushする、という手法である。
 使用中のリポジトリをrepo、元のブランチをhoge、仮に作る手元のブランチをtempとすると、次のようになる。
 手元のtempに対してコミットする。次に、元のブランチを更新する。
 ここで、手元のブランチhogeのリポジトリは最新である。
 ここで、ローカルブランチtempのコミット内容は最新のリポジトリに対するものに置き換わった。ここでコンフリクト(衝突)が生じた場合は訂正が必要だが、訂正後、または運良くコンフリクトしなかった場合は、早速、マージの作業に取りかかる。
 ここで、元々使っていたブランチhogeに、今回コミットに使った仮のブランチtempの内容が反映された。「git log」で履歴を確認することができる。
 そこで、早速リモートリポジトリに対してpushする。
 これで、無事にpushできる率が高まる。
 rabaseとpushの間に割り込まれた場合は面倒が起こることになるが、何もしないよりは面倒が少ない。

設定

設定ファイル
 Gitの設定は、次のファイルに保存される。
 直接書き換えなくても良いように、gitには設定のための機能が用意されている。

個人情報の登録
 インストールしたら、まず名前とメールアドレスを書き込んでおくとよい。
 repoなどを実行して新しいプロジェクトを取得する度に名前とメールアドレスを入力することになるが、登録してあれがEnterするだけで済む。
 $ git config --global user.name "Null Potter"
 $ git config --global user.email null.potter@example.com

エディター
 エディターなら何でも使えるわけではない。一見動いているように見えても、正常に動作しないこともある。
 例えばvimを使いたい場合は、次のように設定したりする。
 $ git config --global core.editor 'vim -c "set fenc=utf-8"'

補足

最初のコミット
 Gitには色々な欠点があるが、一番最初のコミットの修正が面倒であることもその一つである。git reabse -i で戻れるのは2番目のコミットまでで、最初のコミットには戻れない。
 まだ2番目のコミットをしていないのであれば、git commit --amendで書き換えができるが、そうでない場合、次のいずれかの方法を取る。
 別の考え方で、最初のコミットは空にしておく、という方法もある。
 # リポジトリの作成
 $ git init
 # 最初のコミット
 $ git commit --allow-empty -m "最初のコミット"
 完全な空が好みでない場合は、後々の編集は別途コミットで実施する方が望ましいと誰もが思うような、ほぼ空のreadmeをコミットするというのも定番である。
 $ echo "# xxx" >> README.md
 $ git init
 $ git add README.md
 $ git commit -m "最初のコミット"

再検索