Wednesday, August 17, 2011

言うまでもないことだが、タイトルはジョークである。

そもそもバージョン管理は本来我々がしたい事ではない(一部の人を除く)。別に作りたいものがあり、そこでの作業を円滑に進めるためにバージョン管理するのだから、所詮はヤクの毛刈りである。さらに、Gitクライアントのへっぽこさも相まってなかなかに時間を食われる。この文書はそのような人々が、より円滑にGitを使えることを祈って書かれた。

なお、バージョン管理というのはとても複雑なシステムであるため、バージョン管理自体が目的な人には楽しい世界である。そのような人々はぜひGitやその他のバージョン管理システムのマニュアルやソースコードを読んでいただきたい。きっとその奥深い世界を堪能できることだろう。

Git概説

Gitはこれまでの旧来のバージョン管理システムとは一風違った設計で作られている。また、Git特有の概念も多い。なので、まずGitの概観を説明する。

序文の通り、この文書はバージョン管理自体が目的の人向けではない。Gitの内部構造を説明されれば理解できるが、そんな事はどうでもいいので使い方だけ知りたいという人向けの解説である。なので、内部構造には必要な範囲では触れるが必要なければ触れない。

Git

GitとはLinus Torvaldsによって作られた、分散バージョン管理システムである。Linusが作ったのだから当然Linuxで使われている。他にもRailsなどが使っている。

  • git(1)

地名概説

Gitにはデータの保管場所がいくつかある。

  • remote repository
  • local repository
  • index
  • working tree
  • stash

ユーザが変更したデータやリポジトリにあるデータはこれらの間を行ったり来たりする。gitの多くのコマンドは、どの場所のデータを操作するかによって分類されているため、どこからどこに行きたいかを意識することが必要になる。

Working tree

手元にあるgit管理下のディレクトリの中身、それがworking treeである。ユーザによる変更は通常まずここに入り、indexを経てリポジトリへと送られる。 working treeの最上位には.gitというディレクトリがあり、そこにはgitの各種管理情報やlocal repositoryが入っている。

Repositry

リポジトリとはバージョン管理されたものが入っている場所である。ユーザは変更を手元のlocal repositoryに入れ、それをremote repositoryへと送ったり (push)、remote repositoryから他の人が行った変更をlocal repositoryへと取ってきたり (fetch) する。 local repositoryは各working treeの最上位に一つだけ存在する.gitディレクトリの中にある。Gitは分散バージョン管理システムなので、あるリポジトリは基本的に単体で完結している。つまりこのlocal repositoryがあれば、過去の履歴は全て見れるし、ローカルコミットだって出来る。 remote repositoryは通常公開された場所にあるリポジトリである。手元の変更を公開するためや、公開されている変更を手元に取ってきたりする。

  • git-init(1)
  • git-clone(1)

Index

「インデックス」はGit特有の概念であり、一言で言えばコミットのためのステージング環境である。working treeへと行った変更は。通常このindexに一度適用し、その後リポジトリにコミットされる。具体的には、git-addで変更したファイルをworking treeからindexに適用し、git-commitでindexの内容をリポジトリに適用する。 つまり、indexはコミット前の仮置き場として使われることが意図されている。よって、git-diffやgit-checkoutもデフォルトではこのindexを対象としている。

  • git-add(1)
  • git-rm(1)
  • git-commit(1)

Stash

stashはworking treeの一時的な退避場所である。コミットするほどでもない変更を、ちょっと他のブランチをいじりたい時やupstreamに追従して(rebase)変更を続けたい時に用いる。

  • git-stash(1)

Commit

「コミット」はGitの主役である。コミットは以下の三つの情報からなる。

  • ファイルツリー
  • 付加情報 (コミットメッセージやコミットした人など)
  • 直前のコミット (親コミット)

あるコミットがその直前のコミットを知っているという事は、数学的帰納法を使うと、任意のコミットは最初のコミット (initial commit) からそのコミットに至る全ての履歴を知っているという事になる。

コミットIDは上記三つの情報に対するSHA1ダイジェストである。よって、そのコミットのファイルツリーの情報が一致していても、コミットメッセージや直前のコミットが違えば異なったコミットIDになる。逆に言えば、コミットIDさえ知っていれば、ファイルツリーや付加情報、履歴の全てを取り出すことができる。

  • git-commit(1)

Branch

ブランチは一連のコミットへのラベルである。開発版と安定版のように、あるソフトウェアの別バージョンを管理する際に用いる。

一歩踏み込むと、ブランチとは、あるコミット (そのブランチの最新のコミット = branch head) に至る履歴とその将来として理解される。前述の通り任意のコミットはその直前のコミットを持っているため、順に親を辿っていけば履歴が取り出せるわけだ。よって、実装としてもブランチデータの保持は.git/info/refsの1ファイルのみで基本的に足りる。このようにGitはブランチのコストが小さいため、頻繁に利用できるし、する事が推奨される。「トピックブランチ」はその代表だろう。

というわけで、あるブランチにコミットするという行為は、内部的には、

  1. リポジトリに現在のbranch headを親とするコミットを追加
  2. 当該ブランチのbranch head情報を更新

という手続きになる。

  • git-branch(1)

Tag

タグはコミットへのラベルである。タグはリリース時など、ある特定のコミットを指示したい場合に用いる。タグには署名をつけることも出来る。

ブランチとの違いは、タグ付け後に新たなコミットがあっても、タグは同じコミットを指し続ける点である。

  • git-tag(1)

Remote

公開されているリポジトリを手元のリポジトリに取り込んだり、手元のリポジトリを公開する場合は、まずその外部リポジトリの場所をremoteに登録し、それとの操作として扱う。

  • upstreamという名前のremoteを登録

    git remote add upstream git://github.com/ruby/ruby.git
    
  • upstreamという名前のremoteから実際にデータをとってくる

    git fetch upstream
    
  • git-remote(1)

公開

前述のremoteにブランチをpushすることで、そのブランチがremote repositoryに保存される。そのremote repositoryが公開されるように設定されていれば、これで公開が完了する。

remote repository は自分で設定・運用することもできるし、Github等を使ってもよい。

先述のgit-remote(1)で登録した場所に、git push で送信する。

  • git-push(1)

パッチ

  • 作成
    • git-diff(1)
    • git-format-patch(1)
  • 適用
    • git-apply
    • git-am

Pull Request

自分の行った変更を人に取り込んでもらいたい場合、pull requestを行う。

  1. トピックブランチを作る
  2. 変更する
  3. トピックブランチをどこかのremoteにpush
  4. 取り込んでもらいたい相手に変更をpullしてほしいと、remoteのurlを教える

特にGithubのpull requestの場合は、以下の通りになる。

  1. トピックブランチを作る
  2. 変更する
  3. トピックブランチをGithubにpush
  4. GithubのWebページからpull requestボタンを押し、相手にrequestを送る

GithubのWebだけで操作することもできる。

  1. 本家からfork
  2. 編集したいファイルをEdit
  3. pull request

Pull request のマージ

remoteに登録してfetchしてmergeする。

GithubのWeb Interfaceから行う場合、Pull Requests画面から「Merge pull request」のボタン一つでおしまい。

コマンド解説

Gitのコマンドはその名前から想像しづらい機能を持つものが多いので、簡単に解説する。

git-checkout

名前から受けるイメージとはずいぶん違う役割を持つコマンド。実際は”Checkout a branch or paths to the working tree”とある通り、working directoryを操作するコマンドである。主にブランチの切り替えやファイルの取り出しなどで使う。

-b

既存のブランチをもとに新しいブランチを作る場合に用いる

git checkout foo
git checkout -b bar
-t

既存のリモートブランチをupstreamとして新しいブランチを作る場合に用いる。-bを同時に指定しなかった場合、作られるブランチ名はリモートブランチと同じ名前が用いられる。

git checkout -t origin/foo

git-branch

ブランチ管理の際に用いる。working treeのブランチを切り替える際にはgit-checkoutを用いる点に注意。

ブランチの一覧表示

現在のブランチは * で強調表示される。

  • デフォルト/—list: ローカルブランチの一覧を表示する。
  • -r: リモートブランチの一覧を表示する。
  • -a: リモートブランチも含めたすべてのブランチの一覧を表示する。
  • —contains : そのコミットを含むブランチの一覧を表示する。
ブランチの作成

git branch <branch_name> [<start_point>]

を指すブランチを作成する。

ブランチの名前変更

と同じコミットを指すを作る。が指定されていない場合は

git branch [<old_branch>] <new_branch>

ブランチの削除
  • -d …: ブランチを削除する。ただし、そのブランチがupstreamにマージされていない場合はエラーになる。
  • -D …: 問答無用でブランチを削除する。

git-reflog

そのリポジトリのHEADの履歴を表示する、と言うと何に使うのかわかりづらい。rebaseやresetのような履歴に干渉するコマンドであるコミットを見失ってしまったときに使う。reflogで見つけたらcherry-pickする。

git-add

ファイルをindexに追加する。つまり、indexという概念を持つGit特有のコマンド。

see also git-reset and git-rm

-u

更新されたファイルを全てindexに追加する。新規に追加されたファイルはこのコマンドでは追加されないので、git add .などとする。

git-gc

リポジトリを圧縮とかする。

git-rebase

コミット履歴を書き換える。

ローカルで変更した内容を本家に追従させるにはmergeとrebase二通りある。rebaseは手元の変更を常に本家に対する差分として管理したいとき、mergeは手元に本家の変更を随時取り込む形にしたいときに使う。

  1. 追従させたいブランチをcheckout git checkout local
  2. 追従先に対してrebase git rebase origin/master
  3. 手元のコミットが追従先と衝突した場合は二種類から選ぶ。
    • 手元のコミットを修正して使う場合は、修正し、git add してから、git rebase —continue
    • 手元のコミットが必要ない場合は、git rebase —skip

git rebaseはコミット履歴を書き換えるため、つまり履歴を失う可能性がある。万が一のためにrebase前にbranchかtagを作っておくとデータを失うリスクが減る。困った時はgit rebase —abortで元の状態に戻れる。ロストしてしまった気がする場合でもgit-reflogでサルベージできるかもしれない。

-i

コミットを並べ替えたり、複数のコミットを一つにすることができる。

git-reset

HEADまたはindexを操作するコマンドである。git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]という形式の場合、現在のブランチのHEADを操作し、ついでにindexやworking directoryをオプションに応じていじる。それ以外の形式ではindexを操作する。

しかし、git-resetを用いて行われる操作は、大抵の場合git-checkoutを用いるのが正しいように思う。

git-commit

コミットする。

-v

でコミットメッセージ編集時に差分が出るのが便利

—amend

直前のコミットを修正する際に用いる。 直前以外を変えたい時は git rebase -i を用いる。

git-cherry-pick

特定のコミットだけを現在のブランチに取り込む。

see also git-merge

git-blame

誰がその行を変更したか見る

git-grep

リポジトリ内をgrepする。使い方はgrepと同じ。

git-log

コミット履歴を表示する。

-p

コミットメッセージに加えて、各々のコミットでのdiffを表示する

git-svn

Git から Subversion リポジトリを操作する。git svn rebase と git svn dcommit だけ知っていれば問題ないだろう。

.gitconfig に以下の通り書いておくと便利かもしれない。(git svn find-rev ってのもある)

使い始め
git svn clone http://svn.ruby-lang.org/repos/ruby/trunk
最新に更新
git svn rebase
変更の送信
git svn dcommit
Rubyコミッタ向け

卜部さんの記事 を見るべし。

git-stash

working directory の内容をリポジトリに一時保存し、退避させる。

  • git stash save 変更を退避
  • git stash apply 対比しておいた変更を適用

git-diff

その名の通り、diff を取る

—cached

working directoryとのdiffではなく、indexとのdiffを取る。

git-bisect

昔は動いていたのに今はバグって動かない…って時に使う。要するにバグらせたコミットを二分探索して探すコマンドである。

コミットAでは動いていたのに、少なくともB以降動かないという場合、

git bisect start B A

とする。すると、自動的にAとBの中間点Cがcheckoutされるので、バグの再現コードを動かす。ここでは動いたことにしよう。この場合、

git bisect good

を実行する。と、CとBの中間点Dがcheckoutされる (good..A..C D B..bad)。動かなかったら、

git bisect bad

とする。繰り返していくと、最後にバグが入ったコミットが表示される。犯人を突き止めたら、

git bisect reset

でおしまい。

ここまで読んで、「自動化できるんじゃね?」と思った人は鋭い、おもむろにスクリプトを書き始めた人は落ち着け。git bisect run <cmd>というものがあるので、それを使うとちょっと楽になる、かも。

git-imap-send

git-submodule

git-shortlog

git shortlog -ns とか。

git-clean

ワーキングディレクトリから、git に登録されていないファイルを削除します。

  • -d ディレクトリも削除
  • -f 実際に削除(これを指定しないと削除を行わない)
  • -x ignore されてるファイルも削除
  • -X ignore されてるファイルだけ削除
  • -n 削除は行わず、行われる処理を表示

git-merge-changelog

Ruby など、ChangeLogが衝突しまくって困っている人へ。git本体には含まれていないので、別にFreeBSD portsなりhomebrewなりから入れてください。

NetBSDの場合

pkgsrcには2011年8月現在入っていないので、自力で入れないといけません。以下のような感じで入ります。

% git clone git://git.sv.gnu.org/gnulib.git
% cd gnulib
% vi gnulib-tool # NetBSD shでは動かないので、shebangを /bin/ksh にする
% ./gnulib-tool --create-testdir --dir=/tmp/testdir123 git-merge-changelog
% cd /tmp/testdir123
% ./configure
% make
% sudo make install
% cat >> ~/.gitconfig
[merge "merge-changelog"]
name = GNU-style ChangeLog merge driver
driver = /usr/local/bin/git-merge-changelog %O %A %B
% cat >> ~/work/ruby/.git/info/attributes
ChangeLog merge=merge-changelog

.gitconfig

git の挙動は~/.gitconfigでカスタマイズすることができる。

[core]
    pager = less
    editor = vim
[alias]
    ci = commit -v
    st = status
    di = diff
    co = checkout
    br = branch
    l = log --date=local
    lp = log --date=local -p
    ls = log --stat
    find-rev = "!sh -c 'git log -1 --grep=\"^git-svn-id: [^ ]*@${1#r} \" --format=%H' _"
    show-rev = "!sh -c 'git log -1 --grep=\"^git-svn-id: [^ ]*@${1#r} \" -p' _"

    #http://gcc.gnu.org/wiki/GitMirror
    sr = svn rebase
    sci = svn dcommit
    # The current branch.
    cbr = "!f(){ expr $( ( git symbolic-ref -q HEAD || cat $(git dir)/rebase-merge/head-name ) 2>/dev/null ) : 'refs/heads/\\(.*\\)'; }; f"
    # The branch being tracked by the current branch.
    track = "!f() { if p=$(git rev-parse --symbolic-full-name '@{u}' 2>/dev/null); then echo origin/${p##*/}; else git svn info|sed -n 's,^URL.*gcc/\\(branches/\\)\\?\\(.*\\),origin/\\2,p'; fi; }; f"
    # Show all the local commits on this branch.
    lg = "!git log -p `git track`.."
    # Write all the local commits to ~/patch, filtering out modifications to ChangeLog files
    lgp = "!git log -p `git track`.. | filterdiff -x '*/ChangeLog' | sed -e '/^diff.*ChangeLog/{N;d}' > ~/patch"
    # Show all the local changes on this branch as one big diff.
    df = "!git diff $(git merge-base $(git track) HEAD)"
    dfc = diff --cached
    # Reorganize the local commits on this branch.
    rb = "!git rebase -i `git track`"
    rc = rebase --continue
    rs = rebase --skip
    # 'git rmerge mybranch' to reintegrate a temporary branch onto the top of the current branch
    rmerge = "!f(){ cur=`git cbr`; git rebase $cur $1; git rebase $1 $cur; }; f"
[color]
    diff = auto
    status = auto
    branch = auto
    interactive = auto
[merge "merge-changelog"]
    name = GNU-style ChangeLog merge driver
    driver = /usr/local/bin/git-merge-changelog %O %A %B

逆引きGit

いわゆる逆引き

幹Aから分岐した枝Bの差分を見たいとき

  • git diff A…B
  • git log A..B

add してしまったものを取り消し

git addの取り消し、つまりindexの操作であるから、git resetを使う。

現在の作業内容を一時保存

  • git stash save
  • git stash apply

gitで作ったパッチを適用

git-diff の出力するdiffは、パスがa/やb/から始まっているため、patch < foo.diff などとすると上手くパスが認識できない。これはgit-applyを使うか、patch -p1を使えばよい。

バグの混入したバージョンを知りたい

git-bisectを使う。

特定のコミットだけマージしたい

git-cherry-pick

git rebase中になんかわけがわからなくなった

git rebase —abortでrebase前の状態に戻れます。

変更したファイルを元に戻したい

git checkout [<tree-ish>] <pathspec>を使う。

管理外のファイルを消したい

git-clean を使う。

あるリビジョンを取り出したい

特定のコミットをワーキングディレクトリにチェックアウトするには、git-checkoutを用いる。

git push や git pull で衝突した

git push や git pull など remote と通信すると、何事もなくマージされる時と、エラーになることがあります。Fast Forward でないとき、つまり単純な新旧関係ではなく、互いに異なるコミットが混ざっている時にこうなります。何をすると衝突するかいかに例示します。

一人での開発

git commit —amend か git rebase、git reset した時などに起こりえます。

複数人での開発

一人での開発の場合に加え、他の人が何かを push すれば起きます。

ブランチの削除

ローカルブランチ

git branch -dgit branch -D

リモートブランチ

git push <remote> :<branch name> コロンを付けるとか意味わからないよね…。

Notes

  1. biscota reblogged this from takepara
  2. tatsuhico reblogged this from nalsh
  3. tokujoushibire reblogged this from syoichi
  4. mpixy reblogged this from nalsh
  5. wate-wate reblogged this from nalsh
  6. nloc reblogged this from nalsh
  7. nysta reblogged this from nalsh
  8. skoei reblogged this from nalsh
  9. amattresswarehouse reblogged this from nalsh
  10. yashimanote reblogged this from nalsh
  11. katakori reblogged this from nalsh
  12. ukar reblogged this from saitamanodoruji
  13. no-theme reblogged this from syoichi
  14. mostlyfine reblogged this from nalsh
  15. mug-g reblogged this from nalsh
  16. criff reblogged this from saitamanodoruji
  17. knives777 reblogged this from saitamanodoruji
  18. takepara reblogged this from nalsh and added:
    ヤクの毛刈り...。
  19. rousseau reblogged this from ss846
  20. rtfgbhu reblogged this from syoichi
  21. ss846 reblogged this from saitamanodoruji
  22. ume22 reblogged this from saitamanodoruji
  23. kubotomo reblogged this from saitamanodoruji
  24. saitamanodoruji reblogged this from nalsh
  25. hsshkmn reblogged this from syoichi
  26. takurot reblogged this from nalsh
  27. nobby0-0 reblogged this from syoichi
  28. kai-app reblogged this from nalsh
  29. manjiro reblogged this from nalsh
  30. peccu reblogged this from nalsh
  31. youandi051024 reblogged this from nalsh
  32. yoderkeez reblogged this from nalsh
  33. knnr reblogged this from syoichi
  34. iwadon reblogged this from nalsh
  35. ayu reblogged this from syoichi