eyecatch
Sat, Oct 8, 2016

git checkoutを図解する

この記事を読んだ、またはGitのオブジェクトモデルを理解していることを前提に、Gitの git checkout というコマンドについて説明する。 このコマンドは普通ブランチを切り替えるものと説明されるが、主たる機能は オブジェクト格納領域から指定されたファイルを取り出し、ワーキングディレクトリに配置する ものである。 つまりこれがGitにおけるチェックアウトで、チェックアウト=ブランチの切り替えではない。 コマンドに与える引数によっては HEAD の付け替え、つまりはブランチの切り替えもする、というだけ。 git checkout の動作を HEAD の付け替えの有無によって分けて考えると分かりやすく覚えやすいので、以下そのように説明する。 HEADを付け替えないgit checkout HEAD を付け替えない git checkout は、引数にワーキングディレクトリ内の ファイルまたはディレクトリへのパスを与えた場合 のもの。 ディレクトリを指定した場合はそれ以下の全ファイルが操作対象となる。 パスは絶対パスかカレントディレクトリからの相対パスで、複数指定できる。 つまりは以下の様なコマンド形式になる。 git checkout <パス(複数可)> これを実行すると、指定したファイルについて、インデックスが指しているブロブ をオブジェクト格納領域から取り出し、ワーキングディレクトリのファイルを置き変える。 上のスライドではインデックスが指しているブロブを取り出したが、任意のブロブを取り出すこともできる。 この場合、以下の様なコマンド形式を使う。 git checkout <コミット> <パス(複数可)> このコマンド形式だと、指定したコミットが指すツリー以下のブロブ が取り出される。 <コミット>の部分には、コミットオブジェクトのSHA1ハッシュ値、参照(i.e. ブランチかタグ)、シンボリック参照(e.g. HEAD)を指定できる。(実際にはこれらが全てではないが、実用的にはこの3種。) この形式だと、ワーキングディレクトリだけでなく、取り出すブロブを指すよう インデックスも更新される ことに注意。 HEADを付け替えるgit checkout HEAD を付け替える git checkout は、引数に パスを与えない場合 のもの。 代わりにコミットを与える。 つまりは以下の様なコマンド形式になる。 git checkout <コミット> <コミット>の部分には、コミットオブジェクトのSHA1ハッシュ値、参照(i.e. ブランチかタグ)、シンボリック参照(e.g. HEAD)を指定できる。(実際にはこれらが全てではないが、実用的にはこの3種。) これを実行すると、指定したコミットが指すツリー以下の全てのブロブ を指すようインデックスを更新し、それらのブロブをオブジェクト格納領域から取り出してワーキングディレクトリに配置する。 この上更にHEADを付け替えるわけだが、付け替え先は<コミット>の種類によって以下の三通りある。 <コミット>がブランチ: HEADはそのブランチを指すよう更新される。 <コミット>がSHA1ハッシュ値:
eyecatch
Thu, Oct 6, 2016

Gitの良さが分からない? ちょっとそこに座れ

Gitの良さがいまだに分からないという人がいるようなので、Git派の一人としてSubversion(以下SVN)と比較してのGitの良さ(メリット)について語りたい。 (GitとSVNの違いについては他の人の記事に詳しいのであまり書いていない一方、勢い余ってGitのデメリットも書いた。) 本題に入る前に、冒頭にリンクを貼った記事についてひとつだけつっこんでおく。 つっこみどころは他にも沢山あるけど。 ※話の前提としてgitとSVNを採用している現場に下記のような割と違いがあるとする。 git イシューごとにブランチを切り、ローカルでコミットして、リモートブランチにpushして、GitHub・GitLab・Bitbucket経由でマージリクエスト。コードレビューの後にマージ。 SVN リモートのtrunkに個々人が直接コミット。コードレビューはあまりない。ブランチを切ることもない。 このような違いが出る背景には次のものがある。 gitを採用する現場は、猫も杓子もgit-flowというプラクティスに従う傾向がある gitを採用する現場は、コードの品質もある程度管理する傾向がある SVNは集中型でありブランチ機能などが非常に使いにくい SVNを採用する現場はコードの品質よりも「リリースに含めるならさっさとコミット」と考える傾向がある この前提には無理がある。 Gitのところに書いてあるのが、Gitというツールの枠を大きくはみだしたGitHub Flowというブランチ戦略+開発プロセスに当たるものであり、 それでGitを批判するのはお門違いであろうという点については、Gitの流行がGitHubの人気によるところが大きく、GitHubを使えることがGitの大きなメリットであるので、目をつむることにする。(マージリクエストを使う羽目になるデメリットなんて言いがかりでしかないとだけ言っておく。) 看過できないのは、SVNを使った開発がコードレビューもブランチもないという点。 どこの世界の話をしているんだろうか。 Gitが世に出る前は世間にコードレビューもブランチもあまりなかったかのような前提だが、もちろんそんなことは全くない。 60万個以上のOSSプロジェクト情報を統括するOpen HUBによれば、OSSプロジェクトの46%がSVNを使っている。この中にはGitの誕生以降にSVNを使い始めたプロジェクトも多くある。270000余りのプロジェクトの大部分がブランチすら使っていないとでも? GitHub Flowと対比するために無理やりこじつけたんだろうけど、その無理のせいで議論のスタート地点からめちゃくちゃだ。 まともな開発にはコードレビューもブランチも必要だ。 品質管理もリリース管理もしないなら要らないのかもしれないが、そんないい加減な開発現場を前提にSVNかGitかなんて議論しても意味がない。 高品質なソフトウェアを効率よく開発するために則りたい素晴らしい開発フローがあるとして、そのフローをSVNやGitやその他のツールないしひょっとしたらアナクロな日付フォルダの内どれがもっとも上手く実現してくれるか、というのがあるべき議論だ。 この「素晴らしい開発フロー」には一般的に品質管理と並行開発が含まれていて、それらにはコードレビューとブランチの利用が含まれている。 Git(+GitHub)がこんなにも急速にSVNに取って代わって流行ったのは、分散リポジトリの仕組みとブランチの軽量な実装によって効率的な並行開発が実現でき、またプルリクエストなどの機能によりコードレビューを含む快適なソーシャルコーディングが実現できるからだ。 逆に言えば、Gitが流行ったことが、人々が効率的な並行開発やコードレビューを開発フローに取り入れたかった証拠と言えるかもしれない。 Gitのメリット 前置きが長くなったが、少なくともブランチとコードレビューを活用した高品質で高効率なソフトウェア開発をしたいという前提で、SVNに対するGitのメリットを挙げてみたい。 1. リポジトリ構造がシンプル Gitリポジトリはすごくシンプルに作られているそうな。 確かに、その構造を見ると、add、commit、log、resetくらいは自前ですぐに実装できそうだ。 このシンプルな構造のおかげで、Gitリポジトリは壊れにくい。ここで壊れにくいとは、リポジトリ内部で不整合が起こりにくいということで、コマンドミスでコミット履歴が一部消えたりとかいうトラブルは壊れるに入らない。 実のところSVNリポジトリの構造を知らないので経験的なことしか言えないが、SVNリポジトリ(というより作業ディレクトリの管理情報?)はちょくちょく変な状態になり、クリーンアップしたり、酷い時には.svn内のファイルを手動でいじったりしなければならなかった。 因みに、シンプルというのはリポジトリサイズがすごく小さいということにはならず、同等の履歴を含むGitリポジトリとSVNリポジトリはだいたい同サイズなんだそうな。 2. ブランチが軽い Gitのブランチは単一のコミットを指す参照で、リポジトリ内ではSHA-1ハッシュ値が書かれただけのたった一つのファイルに過ぎない。 その為ブランチは一瞬で作成できるし、ディスクも圧迫しないので、じゃんじゃん作ってじゃんじゃん消せる。 さらに、ローカルリポジトリに過去の全ファイルの全バージョンが入っているという分散リポジトリの特長のおかげで、ブランチの切り替えも軽快にできる。 ローカルから必要なファイルを作業ディレクトリに展開するだけなので。 一方SVNはそもそもブランチをサポートする直接的な機能がないため、ブランチはリビジョンのコピーという形で実装されている。 コピーと言ってもハードリンクみたいなものでディスク上に物理的なコピーが作られるわけではなく、軽量という点ではGitと大差ないが、集中リポジトリなせいでブランチの切り替えには差が出る。 svn switchにしろsvn checkoutにしろネットワークの向こうのサーバとの通信が必要なので、それなりの時間がかかるし、通信が途切れると切り替えられなくなる。 冒頭に貼った記事にはGitはブランチを切り替える際にstashとかしないといけなくて面倒とあったが、そんなのSVNだって同じだし、stashすればいいだけだし、stashという機能があるだけSVNよりまし。Gitならコミットはあとから書き変えられるので、stashの代わりに一時的にコミットしちゃってもいい。 それも嫌ならworktree使えばよろしい。 3. バージョン間の差分取得が速い Gitは全てのファイルについて全てのバージョンのコンテンツをまるまるリポジトリに持っている。 一方SVNのリポジトリにはバージョン間の変更が記録されている。 このため、あるファイルについて任意のバージョン間の差分を取るのに、Gitはシンプルにそれぞれのバージョンのファイルを取り出して比較するだけでよいが、SVNは隣り合ったバージョンでなければバージョン間の変更を足し合わせて差分を計算しなければいけない。 さらに、Gitは比較するファイルをローカルリポジトリから取り出すだけでよいが、SVNはサーバへのアクセスが必要なので、差分取得はGitの方が大分速い。 4. ログ取得が速い Gitのコミットは常にプロジェクトの全ファイルに対するものだ。 これは変更したファイルの一部だけを対象とするコミット操作ができないという意味ではない。 Gitがひとつのコミット操作をコミットオブジェクトと呼ばれる単一のファイルに記録し、そのファイルが常にプロジェクトの全ファイルの特定のバージョンを参照しているという意味だ。(正確に言うとこのファイル自身に全ての参照が記録されているわけではないが。) このためGitのコミット履歴は実にシンプルで、ログ一覧を取得するには単にコミットをたどりながらコミットオブジェクトに書かれたログを集めればいい。 一方SVNはファイル毎にバージョンを管理するので、もう少しややこしい。 さらに、Gitはコミットオブジェクトをローカルリポジトリから持ってこれるがSVNは(以下略)。 5. オフラインでだいたいなんでもできる と、ここまで書いて、Gitのいいところはオフライン作業が捗るところではないかと思い立った。 実際Gitは、clone、fetch、pull、pushといったあからさまな操作以外はオフラインでできる。 多くの操作にネットワーク通信コストを払わなくていい上、リモートリポジトリサーバが落ちたりネットワークが落ちたり山に籠ったりしていても作業が続けられる。 ノマドに最適。 一方SVNがネットワーク通信なしでできることは、…ベースバージョンとのdiffくらい?
eyecatch
Fri, Jan 1, 2016

git resetとrevertを図解する

この記事を読んだ、またはGitのオブジェクトモデルを理解していることを前提に、Gitの git revert と git resetというコマンドについて説明する。 この二つはしばしばコミットを取り消すコマンドとして同じ文脈で説明されることが多いのでこのエントリでも一緒に説明するが、実際は全く異なるコマンドだし、そもそもどちらもコミットを取り消すコマンドではない。 git revert git revertは、指定したコミットが持ち込んだ変更を打ち消すコミットを追加する。 リバースパッチを適用すると言ってもよい。 コミットを追加しかしないので、このコマンドによって既存のコミットが消えたり変わったりすることはない。 図にすると以下の感じ。単純。 git reset git resetには二つの機能がある。 インデックスを再設定する(i.e. resetする)機能と、HEADを付け替える(i.e. resetする)機能だ。 インデックスの再設定 インデックスの再設定をするコマンドはgit reset <ワーキングディレクトリ内のファイルのパス(複数可)>。 これを実行すると、指定したファイルについて、HEADが指すコミットが指すツリー内のブロブを指すようインデックスを更新する。 何を言っているのかわからないので図にする。 (この図では便宜的にHEAD、つまり参照をオブジェクト格納領域内に書いているが、実際には別の場所にあることに注意。) 図を見ると、git add Readme.mdとgit reset Readme.mdがだいたい逆のことをしていることがわかる。 要するに、git add <パス>は指定したファイルをステージし、git reset <パス>は指定したファイルをアンステージする。 HEADの付け替え HEADの付け替えをするコマンドはgit reset <コミット>。 これを実行すると、HEADが指しているコミットを指すようORIG_HEADを作成または更新し、指定したコミットを指すようHEADを更新する。 オプションによってはさらにインデックスやワーキングディレクトリを指定したコミットが指すツリーと同期するよう更新する。 このオプションには--soft、--mixed (デフォルト)、--hardの三種類があり、それぞれのオプションを付けた時の更新対象を次の表に示す。 オプション HEAD インデックス ワーキングディレクトリ --soft ○ --mixed ○ ○ --hard ○ ○ ○ この三者の違いについては面倒だしだいたい分かるはずなので図にしないが、git reset <コミット>したときのHEAD動きについて次に図示する。 スライド中でgit reset HEAD^した時点で、コミットDは実質的に削除されたに近い状態になる。 ORIG_HEADという一時的なシンボリック参照で指されているだけで、どの参照からもたどり着けなくなるからだ。 コミットDはいずれgit gcによって実際に削除されるはずだし、git pushしてもコミットD、それが指すツリー、そのツリーの下にしかないブロブはリモートリポジトリに送られない。 よって、git
eyecatch
Thu, Dec 31, 2015

Gitの分散バージョン管理の仕組み

このエントリでは、この記事を読んだ、またはGitのオブジェクトモデルを理解していることを前提に、Gitの分散バージョン管理の仕組みについて説明する。 Gitの分散バージョン管理 分散バージョン管理とは、分散したリポジトリでのバージョン管理ということ。 ここでリポジトリが分散しているとは、同じプロジェクトの履歴を管理する完全で独立したリポジトリが複数あるということ。 これにより一つのプロジェクトの開発を地理的に分散して並行して進めることができる。 Gitは分散バージョン管理のために、リポジトリのクローン(≒コピー)を作る機能と、リポジトリ間でコミットグラフを同期する機能を提供している。 リポジトリのクローンを作ると言うと、オリジナルとクローンの間に格差があるような気がするが、 実際にはGitは全てのリポジトリが対等であるという思想のもとで実装されている。 このため、リポジトリをクローンする時には(デフォルトで)クローン元の完全なコミットグラフがクローンにコピーされるし、任意のリポジトリ間のデータのやり取りをpeer-to-peerでできる。 クローンからクローンを作ることももちろん可能。 git pushでデータを送る先をアップストリームと呼ぶことはあるし、次節でローカルリポジトリとリモートリポジトリという関係が出てくるが、これはあくまでその時点でそういう設定になっているというだけ。 アップストリームはいつでもいくつでもgit remoteコマンドで追加したり削除したりできる。 このような実装により、Gitの分散バージョン管理ではリポジトリ間で柔軟なデータのやり取りができる。 例えば以下の様な複雑なリポジトリネットワークを組むこともできる。 ローカルリポジトリとリモートリポジトリ 一人の開発者から見て、手元にあるリポジトリを ローカルリポジトリ と呼ぶのに対して、git pushやgit pullやgit fetchでデータをやり取りする相手のリポジトリを リモートリポジトリ と呼ぶ。 リモートリポジトリとのやり取りは、リモート追跡ブランチ と リモート というものを使って実装されている。 リモート追跡ブランチ リモート追跡ブランチは、ローカルリポジトリの.git/refs/remotes/に格納される参照で、リモートリポジトリ内のローカルブランチのコミットグラフを取得してローカルリポジトリ内に保持するために使われる。 git branch -rでその一覧が見れる。 「追跡」ブランチというだけあって、リモートリポジトリ内でコミットグラフが成長した場合、この変更に追随することができる。 このためのコマンドがgit fetch。 因みにgit pullは、git fetchでリモート追跡ブランチを更新した後、git merge(オプションによってはgit rebase)でそのリモート追跡ブランチをローカルブランチにマージするのと同じ。 リモート リモートとは、リモートリポジトリのこと、またはリモートリポジトリに接続するための定義のこと。 この定義は、ローカルリポジトリの.git/configにremoteセクションとして書かれている。 以下がその例。 [remote "origin"] fetch = +refs/heads/*:refs/remotes/origin/* url = [email protected]:kaitoy/blog.git セクション名のところに"origin"とあるがこれは、この定義で接続するリモートリポジトリをGitコマンドなどでoriginと指定できるということ。 ここで定義されているのはurlとfetchで、それぞれ以下を意味する。 url リモートリポジトリのURL。 つまり、リモートリポジトリがどのサーバのどのディレクトリにあって、それとのデータのやり取りをどのプロトコルでやるかという定義。 このURLには以下の書式が使える。 ファイルパス /path/to/repo.gitとかC:\\Users\\Kaito\\Desktop\\pcap4jといった、普通のファイルパスの書式。 NFSなどでリモートリポジトリが共有されている場合などに使われる。 シンボリックリンクがサポートされているOS上では、クローンはリモートリポジトリをハードリンクで参照する。 このシンボリック参照でのファイル共有がトラブルの元なため、この書式は非推奨。 ファイルURL file:///path/to/repo.gitとかfile://C:/Users/Kaito/Desktop/pcap4jといった、ローカルホスト上のパスを示すファイルURLの書式。 用途はファイルパスと同様だが、ハードリンクを作る代わりにコピーするのでより安全。 HTTP(S) https://github.com/kaitoy/pcap4j.gitといったHTTPSやHTTPのURL。 リポジトリへのアクセス制御にHTTPの認証機能やHTTPSのクライアント証明書などが使えるほか、HTTPSなら通信の暗号化もできる。 使用するポートがファイアウォールにブロックされていることが少ないのも使いやすい。 Gitプロトコル git://example.com/path/to/repo.gitといった書式で、GitデーモンによるGitネイティブプロトコルを使うURL。 HTTPよりも高速な通信ができるが、認証機能も暗号化機能もない。 SSH + Gitプロトコル ssh:[email protected][email protected]Hトンネルを通してGitプロトコルで通信できる。 Gitプロトコル単体を使うのに比べ、SSHの認証機能と暗号化機能を利用できるが、やや遅くなるはず。 [email protected]:kaitoy/pcap4j.gitのようなSCP書式も使える。 Git自体はGitデーモンを含めリポジトリへのアクセス制御の機能を一切持たないので、認証などが必要な場合はHTTPなどその機能を持つプロトコルのURLを使う必要がある。 fetch リモートリポジトリ内のローカルブランチとローカルリポジトリ内の追跡ブランチとがどう対応するかを定義する。 この定義はrefspecと呼ばれる。 上の例のfetch = +refs/heads/*:refs/remotes/origin/*だと、リモートリポジトリの.git/refs/heads/にある全てのブランチをそれぞれ、ローカルリポジトリの.git/refs/remotes/origin/にある同名のブランチで追跡する、という意味。 クローン時の挙動 クローン時のデフォルトの挙動は以下の様なもの。 オブジェクト格納領域内のオブジェクトが全てクローンにコピーされる。 (多分。参照からたどれないオブジェクトもコピーされることを確認した。) つまり、元のリポジトリ(i.e.