11 Mar 2010

Git入門 ゼロから始めるGitドリル

gitの勉強をしつつ取ったノートを記事化しました。一応これを読めばざっくりとした導入やSVNとの違いが分かってもらえるように書いたつもりです。svnを使った経験があることを前提に進めていきます。

svnの場合、一つのレポジトリに対して認証のあるユーザが変更を報告していくユースケースをとっています。gitの場合は、個々のローカルマシンにリポジトリが分散されて配置され、お互いに変更を報告しあうユースケース。これはLinuxの伝統的なバザール方式の開発を想定しています。そのため例えばカフェや電車で開発したり、マスターはgithubやgitfarm(Git Hosting参照)にしておいて時々ローカルの変更を報告することも可能です。


目次







インストール


インストール方法はOSに応じて行ってください。
Mac
Ubuntu
Windows


基本操作



Gitリポジトリの作成


$ mkdir ~/workspace/git
$ cd  ~/workspace/git

# 初期化
$ git init
$ ls .git
branches  config  description  HEAD  hooks  info  objects  refs
$ echo trial > test.txt

# 索引の追加
# 索引はコミットする前のステージのようなもの。
# 索引はgit add によって繰り返し更新可能
# git add .はカレントディレクトリ以下のディレクトリ/ファイルを再帰的にスナップショットに追加します
$ git add .
# 削除したファイルを索引に追加する場合は -uが必要。例, git add -u path/to/files/no/longer/necessary 

# メッセージを指定してコミット
$ git commit -m "initial commitment"

# ブランチの一覧を表示
$ git branch
* master #=> masterというブランチは初期状態で作成されます
$ git log
commit e1bf45812b9eedb8aa578af3c0e87a96d84cf3b6
Author: bob 
Date:   Fri Mar 12 00:28:59 2010 +0000

initial commitment



場所を移動してもgitは動作します。レポジトリは作業ディレクトリと同じ場所にあります。
$ mkdir trial
$ mv test.txt trial/
$ mv .git trial/
$ cd trial

# gitが動作している事を確認
$ git show #最後のコミット情報が表示される


こまめにコミットすることは個人で開発を進めている時にも非常に有効です。subversionと大きく違うところは個人でリポジトリを所有しているところです。マスターになるリポジトリにpush(後述)しない限り未試験のコードをローカルのリポジトリにコミットすることができます。例えば、複数の実装方法を思いついたときに現在の内容を一時的なソース保管庫としてリポジトリにコミットしておいて、ある方法が失敗したと思ったらすぐ元の位置に戻ればよいわけです。

こまめなコミットによってローカルのlogが汚れるのが嫌な場合は、tag(後述)を使用したり、公開用のリポジトリをもう一つ用意することによって対処できます。Gitを採用するのならこのアドバンテージを利用すべきでしょう。折角subversionから切り替えるのなら気持ちも一緒に切り替えると使い方の幅も広がるのではないでしょうか。


ブランチの作成




$ git branch new 
$ git branch
*  master #=> *は現在どのブランチを参照しているかを示しています
new

# svnのcheckoutはリポジトリから最新を取得しますが、
# gitのcheckoutはブランチを切り替えてその最新を取得します。
$ git checkout new
Switched to branch 'new'
$ git branch
master
* new
$ git checkout master

# ブランチの削除
$ git branch -d new


ブランチの作成(2)
# 新しくブランチを作成してチェックアウトします。= git branch new + git checkout new
$ git checkout -b new
$ echo ' of git' >> test.txt

# 変更をリポジトリに報告
$ git add .

# 索引と最後のコミットを比較
$ git diff --cached
diff --git a/test.txt b/test.txt
index 10b4e1b..48215e2 100644
--- a/test.txt
+++ b/test.txt
@@ -1 +1,2 @@
trial
+ of git

# 今どんな状態?
$ git status
# On branch new # newブランチ上に
# Changes to be committed: # コミットすべき変更有り 
#   (use "git reset HEAD ..." to unstage)
#
# modified:   test.txt #更新: test.txt
#
$ cat .git/COMMIT_EDITMSG 
modified test.txt
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch new
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
# modified:   test.txt
#

# -mオプションを省略するとgit diffと同様に
# commitによってどのような変更がなされるか#付きのコメント行で示される。
$ git commit
modified test.txt
# On branch new 
# Changes to be committed: 
#   (use "git reset HEAD ..." to unstage) #unstage(索引を空に)したい場合はgit rest HEADしてね
#
# modified:   test.txt 
#

svnのようにbranch用のディレクトリをこしらえる必要は無い。
gitではブランチを切り替えることで作業ディレクトリの状態を切り替えます。

現在のブランチは.git/HEADで管理されています。
$ cat .git/HEAD
ref: refs/heads/new
$ cat .git/refs/heads/new
bdfb3c51bba2c6d60e7bc326ea686702849c21ab
$ git show
commit bdfb3c51bba2c6d60e7bc326ea686702849c21ab
$ cat test.txt
trial
of git
$ git checkout master
$ cat .git/HEAD
ref: refs/heads/master
$ cat test.txt
trial

# 作業ディレクトリにnewブランチの変更を引き込む
$ git pull . new
$ cat test.txt 
trial
of git
$ gitk



タグ



$ git tag v0.10.00

# タグの一覧表示
$ git tag -l
v0.10.00
vx.xx.xx
:
vx.xx.xx
# タグはいろんな箇所で使えます
$ git diff v0.10.00 ORIG_HEAD
# v0.10.0の位置に "stable" という名前の新しいブランチを作成
$ git branch stable v0.10.00 



ファイルを無視する



$ mkdir bin
$ touch bin/gabage
$ git status
# On branch master
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
# bin/ => binが追跡されてないのでgit addして と言ってる
nothing added to commit but untracked files present (use "git add" to track)

$ vi .gitignore
bin/*                                                                               */  
$ git status
# On branch master
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
# .gitignore => .gitignoreもgitの管理対象
nothing added to commit but untracked files present (use "git add" to track)

$ git add .gitignore
$ git commit
Added new file .gitignore           
$ git status
# On branch master
nothing to commit (working directory clean)



索引の理解


コミット - Gitでは履歴内にある1点のことを指す。プロジェクトの全履歴は 相互に関連したコミットの集合により表現されています。git では"コミット"という言葉を 他のリビジョン管理システムが使用する "リビジョン" または "バージョン"と同じ意味で 使用することがあります。また、コミットオブジェクト の略称として使われることもあります。

索引 - コミットしたいものの追跡を保つ為、git は "索引(index)" と呼ばれる 特別なエリア内にツリーの中身のスナップショットを保管しています。

コミット[HEAD]

↑ git commit

索引[index]

↑ git add

作業ディレクトリ[working tree]

$ echo 1 > diff.txt
$ git add diff.txt
$ echo 2 > diff.txt

# 索引 と 最後のコミットを比較
$ git diff --cached
diff --git a/diff.txt b/diff.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/diff.txt
@@ -0,0 +1 @@
+1

# 作業ディレクトリ と 索引 を比較
$ git diff
diff --git a/diff.txt b/diff.txt
index d00491f..0cfbf08 100644
--- a/diff.txt
+++ b/diff.txt
@@ -1 +1 @@
-1
+2

# 作業ディレクトリ と 最後のコミット を比較
$ git diff HEAD
diff --git a/diff.txt b/diff.txt
new file mode 100644
index 0000000..0cfbf08
--- /dev/null
+++ b/diff.txt
@@ -0,0 +1 @@
+2



取り消し




導入

一口に変更を元に戻すといってもシングルリポジトリなSVNと異なり以下にあげたようにいくつかの場面があります。これは索引やコミットそしてリモートなどの仕組み上の違いによるものです。

- 一個のファイルの変更を取り消して最後のコミットの状態にする
git reset -- filename

- 最後のコミットの内容に索引と作業ディレクトリを戻す
git reset --hard HEAD

- 最後のコミットと索引と作業ディレクトリをその前のコミットに置き換える
git reset --hard ORIG_HEAD

- 最後のコミットの後に、その前のコミットの状態をコミットする
git revert HEAD

以下に違いを解説します。でもその前に --hardっていうオプションはなんなんでしょうか?--hardがあるなら--softもあるのか?

--hardと--softの違い


$ git reset -h
--mixed reset HEAD and index                
--soft  reset only HEAD                     
--hard  reset HEAD, index and working tree 
--merge reset HEAD, index and working tree


一個のファイルの変更を取り消して最後のコミットの状態にする

きっと一番よく使うのがこれ。コミットしていない変更がある場合は単にgit checkout filenameとしてもファイルの内容は最後のコミットの状態には戻らない。git reset --hardは作業ディレクトリ以下を全て最後のコミットの状態に戻してしまう。そんな時は '--'オプションをつけると一個のファイルだけを戻すことができる。
$ git checkout -- filename
参考


このコマンドで削除したファイルをリポジトリから復活するもできます。
$ git status
# On branch master
# Changed but not updated:
#   (use "git add/rm ..." to update what will be committed)
#   (use "git checkout -- ..." to discard changes in working directory)
#
# deleted:    rerun.txt

$ git checkout -- rerun.txt
$ ls
rerun.txt

間違ったコミットを無かったことにする


$ touch abigmistake
$ git add .
$ git commit -m 'a big mistake'
$ git log
commit b37c941bfe9364b681c8b760397b14e846b8fc7d
Author: suzukimilanpaak 
Date:   Fri Mar 19 17:05:55 2010 +0000

a big mistake

commit bdfb3c51bba2c6d60e7bc326ea686702849c21ab
Author: bob 
Date:   Fri Mar 12 01:08:36 2010 +0000

modified test.txt

$ git reset --hard ORIG_HEAD

# 以下のように特定のタグを指定することもできる
$ git reset --hard v0.10.00

#注意!: 作業ディレクトリの変更も削除されてしまう
$ ls
bin test.txt

$ git log
commit bdfb3c51bba2c6d60e7bc326ea686702849c21ab
Author: bob 
Date:   Fri Mar 12 01:08:36 2010 +0000

modified test.txt

commit e1bf45812b9eedb8aa578af3c0e87a96d84cf3b6
Author: bob 
Date:   Fri Mar 12 00:28:59 2010 +0000

initial commitment


注意! git reset --hard ORIG_HEADはリポジトリを直接変更します。誤ったコミットの履歴HEAD(この場合 'a big mistake')を取り除きます。変更を取り消した履歴を残すことはありません。そのため他の作業者がそのコミットを既に取り寄せていた場合親族関係が崩れてしまい、他の作業者の変更がどこにも到達不可能になってしまうというケースが起こりうります。そのためgit reset --hard ORIG_HEADを行う場合は他の作業者に事前の確認が必要です。


間違ったコミットを無かったことにするが、ファイルへの変更をリセットしない


$ git reset --soft HEAD^

コミットに含んで欲しくない変更があった場合や、タイポを後で見つけてしまった場合、新たな変更をコミットに含みたい場合などに便利
参照


最後のコミットを取り消して指定したコミットの状態に戻す。そして、その取消しをコミットとして履歴に残す



他の作業者がすでに間違ったコミットを取り寄せ済みの場合、間違ったコミットの変更を取り消すコミットを上書きする方法が望ましいようです。単に git revertに間違ったコミットへの参照を渡すことによって変更を取り消す新しいコミットが作成されます。 また、新しいコミットに対するコミットメッセージが促されます。

$ echo foo > test.txt
$ git commit -am 'Modified test.txt'
# HEADをORIG_HEADの内容に戻す。コミットも同時に行われます
$ git revert HEAD

Revert "Modified test.txt"

This reverts commit 07abe15e79f4c1429fb1d22247723e29dc50293a.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       modified:   test.txt
#

$ git log
commit 42b30ec986d7fdb8d76c7a5b7c89f17114e8039c
Author: bob 
Date:   Fri Mar 12 01:45:28 2010 +0000

Revert "Modified test.txt"

This reverts commit 07abe15e79f4c1429fb1d22247723e29dc50293a.

commit 07abe15e79f4c1429fb1d22247723e29dc50293a
Author: bob 
Date:   Fri Mar 12 01:45:04 2010 +0000

Modified test.txt

#ここまでの変更をGUIで表示
$ gitk &


コミット前の変更を取り消す - git reset HEAD


索引を空にする
$ touch abigmistake
$ git add .
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage) #unstageする場合はgit reset HEADしてね
#
# new file:   abigmistake
#
$ git reset HEAD abigmistake

# 索引から履歴は削除されるがファイル自体は残っている
$ ls
abigmistake  test.txt

# もう一度索引に追加することもできます
$ git add abigmistake

$ git reset --hard HEAD
$ ls
test.txt


共同作業 - 2人




準備
# aliceの署名情報を設定
$ git config --global user.name "alice"
$ git config --global user.email "engineerflies+alice@gmail.com"
$ cat ~/.gitconfig
[user]
name = alice
email = engineerflies+alice@gmail.com
$ cd ..
$ mkdir alice && mv trial alice
$ git clone ./alice bob
bob$ cd bob
# bobの署名情報を設定(この場合同じユーザで書名情報を設定しているので~/.configを上書きする)
bob$ git config --global user.name "bob"
bob$ git config --global user.email "engineerflies+bob@gmail.com"

bob$ echo bob edited >test.txt
bob$ git commit -am "bob edited"
alice$ echo alice edited > test.txt
alice$ git add .

# bobのmasterブランチを現在のaliceのmasterブランチに取り寄せる
# 変更情報のみがリモート追跡用ブランチに格納されます。test.txtは変更されない
alice$ git fetch ../bob master
alice$ cat test.txt 
alice edited

# aliceのHEADとbobからpullしたFETCH_HEADの差異を表示
alice$ git diff HEAD..FETCH_HEAD
diff --git a/test.txt b/test.txt
index 48215e2..4cba843 100644
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1 @@
-trial
- of git
+bob edited
alice$ cat .git/FETCH_HEAD 
b61671548ad431d9d865f0dd7eaeded68ec23711  branch 'master' of ../bob
alice$ git merge FETCH_HEAD
Updating 7d62c24..b616715
error: Entry 'test.txt' would be overwritten by merge. Cannot merge.

#このコミットはコンフリクトを起こさない(HEADと作業ディレクトリを比較しているから?)
alice$ git commit -am "alice edited"
alice$ git log
commit d04f56226dde3ac67839a5695ab6fb5644a34bc9
Author: bob 
Date:   Thu Mar 18 13:21:13 2010 +0000

alice edited

commit 7d62c24953010a7628b29e891405e973b24a4239
Author: bob 
Date:   Fri Mar 12 01:44:29 2010 +0000

ignore bin/
:
:

# 再度merge - コンフリクトを起こします
alice$ git merge FETCH_HEAD
Auto-merging test.txt
CONFLICT (content): Merge conflict in test.txt
Automatic merge failed; fix conflicts and then commit the result.
このようにマージは常に直前にコミットが済んでいることを想定して行われる様です。
dirtyな状態(現在のブランチにコミットされていない変更が作業ディレクトリ内に含まれていること)ではマージは行われません。

#コンフリクトを手動で回収します
alice$ vi test.txt
bob edited then alice edited

結果の表示
# bobが最後にpullしてから、aliceがbobの変更をpullするまでの変更を表示する
$ gitk HEAD..FETCH_HEAD
# aliceとbobのそれぞれの変更のうちお互いに'到達不可能な'変更を表示する
$ gitk HEAD...FETCH_HEAD

alice$ git commit -a
Merge branch 'master' of ../bob #マージ用のログを自動生成してくれます

Conflicts: #コンフリクトが test.txtにあった
test.txt
#
#あなたはマージをコミットしている様です(とgitは認識しています)
# It looks like you may be committing a MERGE. 
#これが正しくなかったら、.git/MERGE_HEADを削除してからもう一回トライしてね
# If this is not correct, please remove the file 
#       .git/MERGE_HEAD
# and try again.
#

#以下はいつもコミット時に表示されるメッセージと同じ
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       modified:   test.txt
#

#最後にaliceの変更をbobにも取り寄せておきます
#pull は fetch + mergeと等価です
bob$ git pull ../alice master



自分の変更を隠す


bob$ echo love letter from canada >> testto.txt 
bob$ git commit -am "love letter"
alice$ echo affair in us>>test.txt 
alice$ git pull ../bob
# stash: 隠しもの
alice$ git stash save affair
# 隠しものを一覧する
alice$ git stash list
stash@{0}: On master: affair
alice$ git pull ../bob
alice$ vi test.txt 
love letter from canada
love letter from us
alice$ git commit -am "in love"
alice$ git stash clear



リモートブランチ


今までの作業でaliceがgit pull ../bobした場合はbobをリモートとして登録することなくbobの変更をaliceにマージしていた。bobとaliceの共同作業が恒久的に行われる場合はリモートブランチとしてbobを登録してしまった方が便利だ。この方法によってリモートにあるブランチをローカルのブランチであるかの様にマージができます。
bob$ echo bob edited > test.txt 
bob$ git commit -am "bob eidted"
alice$ git remote add bob ~/workspace/git/bob
alice$ git branch -r
bob/master
alice$ cat .git/config 
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "bob"]
url = /home/suzukimilanpaak/workspace/git/bob
fetch = +refs/heads/*:refs/remotes/bob/*                             


# 変更情報のみがリモート追跡用ブランチに格納されます。test.txtは変更されない */
alice$ git fetch bob

# 自分のmasterとbobのmasterを比較
# -p はpatchの略、patch提出用のログメッセージを出力してくれます
alice$ git log -p master..bob/master
commit 460f2c8928b7f1b1065766c6c2652f79001a054c
Author: bob 
Date:   Thu Mar 18 14:01:11 2010 +0000

bob eidted

# -pによって出力される変更ログ
diff --git a/test.txt b/test.txt
index 843ad85..4cba843 100644
--- a/test.txt
+++ b/test.txt
@@ -1 +1 @@
-love letter from canada
+bob edited
alice$ git merge master bob/master
Already up-to-date with ed79b27cc53885e29b2770a9f20d8fb2174e48b6
Trying simple merge with 460f2c8928b7f1b1065766c6c2652f79001a054c
Simple merge did not work, trying automatic merge.
Auto-merging test.txt
ERROR: content conflict in test.txt 
fatal: merge program failed
Automatic merge failed; fix conflicts and then commit the result.

# コンフリクトを解消します
alice$ vi test.txt
love letter from canada
love letter from us
bob edited
alice$ git commit -a
Merge branch 'master'; commit 'bob/master'

Conflicts:
test.txt

# bobのリポジトリはaliceをcloneして作られたためoriginにaliceのリポジトリが指定されている。
bob$ cat .git/config 
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*                                          */
url = /home/suzukimilanpaak/workspace/git/./alice
[branch "master"]
remote = origin
merge = refs/heads/master
bob$ git config -l
user.name=bob
user.email=sin.wave808+bob@gmail.com
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*                             */
remote.origin.url=/home/suzukimilanpaak/workspace/git/./alice
branch.master.remote=origin
branch.master.merge=refs/heads/master
bob$ git branch -r
origin/HEAD -> origin/master
origin/master
origin/new

# git pullで引数を省略すると remote "origin"に登録されている変更を取り寄せる
bob$ git pull

# ちゃんと変更されてる
bob$ cat test.txt 
love letter from canada
love letter from us
bob edited




履歴の検索


履歴の検索に強くなっておくと今後の作業に便利です。

$ git log
commit 74f846a1bde25ef729b1742bfcd9fc693da1dbda

# ある時点のコミットの詳細を表示
# SHA1値の全てを指定する必要は無い
$ git show 74f846a1bde25ef
commit 74f846a1bde25ef729b1742bfcd9fc693da1dbda
Merge: 17b2308 a01e7b7
Author: suzukimilanpaak 
Date:   Fri Mar 5 17:25:15 2010 +0000

tip of collaboration


$ git show HEAD^  # HEAD の親を表示
$ git show HEAD^^ # HEAD の祖父を表示
$ git show HEAD~4 # HEAD の4つ前を表示

# "love"を検索
$ git grep love
test.txt:love letter from canada
test.txt:love letter from us

# "love"をタグv0.00.00から検索
$ git grep love v0.10.00 #=> 何も見つからない
$ git log v0.10.00              # v0.10.00以降のコミット
$ git log --since=yesterday     # 昨日からのコミット
$ git log --since="3 hours ago" # 3時間前からのコミット

# 最近2週間に "drivers" ディレクトリを修正したコミットでgitkで表示
$ gitk --since="2 weeks ago" drivers/ 

# ファイル名を指定
$ git diff v0.10.00:test.txt HEAD:test.txt

# 変更したファイル名を検索
$ git log --stat|grep -A 5 haml
changed the view rendering engine html to haml.

app/views/feeds/new.html.haml |   48 ++++++++++++++++------------------------
1 files changed, 19 insertions(+), 29 deletions(-)



差分の表示
$ git checkout -b a
$ echo a > a.txt
$ git commit -am "a"

# aにあってmasterにないコミットを表示
$ git log master..a
commit b090846acd519c2066a8975ed4c76de700d66fc3
Author: bob 
Date:   Thu Mar 18 14:44:04 2010 +0000

a

#masterにあってaにないコミットを表示 => 何も表示されない
$ git log a..master



共同作業 - 2人 + ルート


今まではbobがaliceをoriginとして参照する方法で行って来ました。実際の開発ではルートとなるレポジトリを設けてアリスとボブがそれを参照する方が管理を行いやすいです。

/opt$ mkdir git

# rootというリポジトリをaliceからコピーして生成します。
# この作業の前にaliceが開発の先端になっているべきであることに注意してください。
# bare"裸の"とは作業ファイルがなく、管理情報だけを持つリポジトリという意味
# sharedを付けると共用のユーザ権限がリポジトリに与えられます
/opt$ git clone --bare --shared ~/workspace/git/alice/ root
alice$ git remote rm bob

# -t: master ブランチをtrack"追跡"します
alice$ git remote add -t master origin /opt/git/root
alice$ git config -l
remote.origin.url=/opt/git/root
remote.origin.fetch=+refs/heads/master:refs/remotes/origin/master
# fetchがrefs/heads/masterを参照する様に設定されています
# -tオプションが指定されていない場合はrefs/heads/*が設定されます */

# 引数なしでpullをするとremotes/origin/masterから変更を取り寄せます
# git remote add に-tを使用したためです。
alice$ git pull

# これまでこの記事にそってリポジトリを作成してきた場合originはaliceを参照しているはずです
bob$ git config -l
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*                                 */
remote.origin.url=/home/suzukimilanpaak/workspace/git/./alice
bob$ git remote rm origin
bob$ git remote add -t master origin /opt/git/root
bob$ git pull


bob$ echo love from vancouver > test.txt
bob$ git commit -am "love from vancouver"
bob$ sudo git push origin master

alice$ echo love from san francisco > test.txt 
alice$ git commit -am "love from san francisco"
alice$ sudo git push origin master
To /opt/git/root
! [rejected]        master -> master (non-fast forward) 
error: failed to push some refs to '/opt/git/root'


fast forwardじゃないというメッセージがでてpushが拒絶されます。
fast forward - 今行おうとしているコミットが既に他方のコミットに含まれている場合、gitは現在のブランチの先頭をマージされるブランチの先頭の位置に進め、新しいコミットは作成されません。これをfast forwardといいます。

この場合、aliceの変更はbobが先に行った変更と衝突するため拒絶されています。別の言い方をすると、共有リポジトリを置いて開発を行う場合fast forwardでない限りpushを受け付けてもらえないようです。

alice$ git pull
CONFLICT (content): Merge conflict in test.txt
alice$ vi test.txt
love from vancouver
love from san francisco
alice$ git commit -a
alice$ git push origin master

今度はpushを受け付けてもらえました

root$ git log
commit 40c4ded3df30d696a16552d928b3a46515cfb516
Merge: f180d66 a2bcc0d
Author: bob 
Date:   Thu Mar 18 17:00:51 2010 +0000

Merge branch 'master' of /opt/git/root

Conflicts:
test.txt

commit f180d664d2943ccc83075e7dc1300f7dca999d72
Author: bob 
Date:   Thu Mar 18 16:40:37 2010 +0000

love from san francisco

commit a2bcc0d98f0e2c98d37df233aecb30caacb028f5
Author: bob 
Date:   Thu Mar 18 16:39:49 2010 +0000

love from vancouver



- 3つ以上のマージ
http://www8.atwiki.jp/git_jp/pub/Documentation.ja/user-manual.html#merging-multiple-trees

githubを使う


githubをマスターレポジトリとしてそこにローカルの変更をpushする環境を構築しましょう。まずはgithubでユーザアカウントを作成してください。githubでレポジトリを作成するとリポジトリのURLを教えてくれるのでそれを今まで通りgit remoteで登録します。そしてgithubにsshの公開鍵を渡し、ローカルの変更がうまく反映されるまでの流れを説明します。

- こちらにアクセスしてください。http://github.com/repositories/new
- 必要事項を書き込みます
- ローカル環境のセットアップの説明が表示されます

説明ページの手順にそって作業を進めていきます。

$ cd ~/.ssh
既存の鍵ペアがある場合はバックアップをとります
$ mkdir key_backup
$ cp id_rsa* key_backup
$ rm id_rsa*
鍵ペアを生成します
$ ssh-keygen -t rsa -C "tekkub@gmail.com"

鍵のファイル名はレポジトリごとに指定するのが後々管理しやすいです。ここではid_rsa_githubを指定しました。

ホスト毎にsshが読み込む秘密鍵が分かるように以下の設定を~/.ssh/configに書き込みます。
$ vi config
Host github.com
HostName github.com
User git
IdentityFile /home/suzukimilanpaak/.ssh/id_rsa_github

接続可能か試します。Permission denied (publickey).が表示されず、パスフレーズを求められれば上記の設定が成功しているはずです。
ssh -v git@github.com

公開鍵をコピーします。改行が入らないようにxclipを使用します
$ sudo apt-get install xclip
$ cat ~/.ssh/id_rsa.pub | xclip -sel clip
xclipでクリップボードに公開鍵の内容が保存されているので、それを以下のページのKeyという項目に張り付けます
https://github.com/account#ssh_bucket
titleの項目は任意です。

~/workspace/git$ git clone alice snippets
~/workspace/git$ cd snippets
リポジトリのアドレスは作成したリポジトリのページからコピーしてください
snippets$ git remote add -t master origin git@github.com:yourusername/code-snippets.git
snippets$ git pull


READMEを作ってみましょう
snippets$ vi README
This repo is for my private use only. thank you
何だかgithubの意に反したことを言っていますが。。。

snippets$ git add .
snippets$ git commit -am "added README"
snippets$ git push origin master


- Network Graphを覗いてみましょう。今までの変更がグラフィカルに表示されているはずです。
http://github.com/yourusername/code-snippets/network
すばらしい

このセクションを読んだあとに山形浩生さんが公開されているEric Raymondの伽藍とバザールの日本語訳をご覧になられると良いかもしれません。このページの読者にはこれからオープンソースの開発に携わってみたいと思っている人も少なくないでしょう(私もその一人です)。githubの空気、ノリをつかむにはちょうど良い資材だと思います。


Git + Apache + Basic認証


エンタープライズな開発の現場でもgitが使われるといいですね。Apacheと組み合わせて使う方法をご紹介します。svnと基本はあまり変わらないです。2009年末にapacheの連携を解消するsmart HTTPが発表されました。Git > v1.6.6、Apache 2.x という環境で恩恵をうけることができますが、下位互換がありますのでこの記事を書くに当たってそちらの構成を採用しました(これについては書き掛け)。

Ubuntuでの設定をご説明します。まずはApacheが参照するリポジトリをDAV越しにローカルリポジトリが操作できるようにするところまで説明します。Smart HTTPについては後述します。

認証方式について
Gitは認証方式の指定ができません。そしてBasic認証を行おうとします。http.authanyで全てのHTTP認証方式に対応できるようですが都度で認証方式を決定するまでリトライするので重いようです。これではSmart HTTPを採用した意味がないので使用は避けた方がいいでしょう。


# DocumentRootの場所を探します
$ grep -r DocumentRoot /etc/apache2/*                                               */
# DocumentRootに移動
$ cd /var/www
$ sudo mkdir dev.git
# 裸のgitリポジトリを作成します。apacheユーザでアクセスするので--sharedオプションは使用しません。
$ sudo git --bare init
$ git update-server-info

# apacheユーザを検索(apacheが動作している前提)
$ ps aux |grep apache
www-data  1862  0.0  0.0  40064    52 ?        S    09:10   0:00 /usr/sbin/apache2 -k start
$ sudo chown -R www-data:www-data dev.git
$ cd /etc/apache2


Ubuntuでapache2をapptitudeやapt-getからインストールした場合、/etc/apache2/mods-available/dav.loadがあるはずです。特に設定の変更は必要ありません。

次にDAVLockDBディレクティブがあるか確認します。ms_dav_fsはユーザの操作をロックするためにSDBMを使用します。そのためこのディレクティブによってデータベースファイルが指定されている必要があります。
$ grep -r DAVLockDB mods-available/*                                            */
mods-available/dav_fs.conf:DAVLockDB /var/lock/apache2/DAVLock
モジュールを有効にします。
$ sudo a2enmod dav dav_fs


バーチャルホストの作成
$ sudo vi sites-available/git
NameVirtualHost *
<VirtualHost *>
ServerName <servername>
ServerAdmin a.hi.tech.hippie@@googlemail.com
DocumentRoot /var/www/dev.git
ErrorLog /var/log/apache2/error.dev.git.log

<Location />
DAV on
AuthType Basic
AuthName 'developer'
AuthUserFile /etc/apache2/.htbasic
Require valid-user
</Location>
</VirtualHost>

$ sudo vi /etc/hosts
127.0.0.1       <servername>
127.0.1.1       <servername>

$ htpasswd -c /etc/apache2/.htbasic <user>
$ /etc/init.d/apache2 restart

ブラウザでhttp://<servername>/にアクセスして認証が通るか確認してください。失敗した場合はErrorLogディレクティブで指定したログファイルを参照しながら修正してください。

gitがapacheにアクセスできるように ~/.netrcファイルに次の記述をして作成してください。~/netrcはオーナーだけが読み書きできるように権限に600を設定してください。
machine <servername>
login <user>
password <password>



それではApache上のリポジトリからローカル作業用のcloneを作りましょう。
~/workspace/git$ git clone http://<servername>/ dev


もしこれが動作しない場合、curlを使って認証が通っているか確かめてみましょう。
curl --netrc --location -v http://<username>@<servername>/HEAD


初めてのプッシュ
~/workspace/git/dev$ touch work?
~/workspace/git/dev$ git add .
~/workspace/git/dev$ git commit -m "created work?" 
# masterブランチの内容をoriginにpush
~/workspace/git/dev$ git push origin master
Fetching remote heads...
refs/
refs/tags/
refs/heads/
updating 'refs/heads/master'
from 0000000000000000000000000000000000000000
to   bd4637936d5b8978b82ef06f8a8b6f75b716552f
sending 2 objects
done
Updating remote server info

うまくいかない場合はApacheのリポジトリの所有者やwriteの権限、それからgit update-server-infoを実施したか再チェックしてみてください。

ここまででGit + Apache + Basic認証の設定は完了ですが、この構成で開発を続けていくときっとpushやfetchが遅いことに気づくと思います。それはGitがHTTP越しに動作するときにPackfile(Gitのストレージファイル)をまるごと転送しないといけないためです。Smart HTTPではupload-packとreceive-packというApache上で動くCGIを用意しており、それらがおあつらえのpackfileを作成し、一連のpostを行うようです。GETパラメータにSmart HTTPを実施するかどうかが渡されるため、対応していない場合単純に無視され旧バージョンの動作をします。この設定に関しては後日追記するつもりです。それまでこちらの記事を参考にしてください。


この文章で扱ってないこと
- ダンプ

この文章は著者の理解が甘いため時々修正したり、書き足したりして縦に伸びていく予定です。ツッコミ、文の分かりづらいなどの指摘お待ちしております。


.

8 Mar 2010

Ruby実効環境の簡単なまとめ

自分用のメモです。

apache + mod_proxy_balancer + mongrel cluster


mongrelは初期状態では単一プロセスに単一のRubyインスタンスを持ってーつのポートを占有する。
mongrel clusterを使うことによって複数のプロセスを立ち上げることができる。プロセス数の分ポートを占有する。

mod_proxy_balancerはapacheに対するアクセスをバランシングしてくれる。この場合、1つのポートに対するアクセスを複数のmongrel clusterのポートにバランシングしてくれる。

nginx + unicorn


nginx[エンジンエックス]は軽量な軽量高性能なWebサーバ/リバースプロキシ。リバースプロキシとしてapacheを立てるまでもない場合に使用すると便利。

unicornはprefork型のweb server。Unicorn masterが起動するとまずプログラミングコードロードする。その後workerをforkする。workerがいくつあってもコードのロードは1回きりなので起動が早い。nginxからはunix socket通信で対話するいわばカーネルによるロードバランシング。

参照

apache + passenger


Apacheのprefork MPMとphusion passengerの組み合わせ。passengerはapacheの動的モジュールとして提供されており、スポーンサーバにアプリケーションのコピーを要求する。forkされたworker x複数 とspawner x複数がやりとりをする形。

参照

間違いがあったら教えてください。

.

1 Mar 2010

Cucumber入門(3) 一番最初のCucumber on Rails

Cucmberはセットアップの時点でいろいろと選択肢があるようです。まずは使ってみましょう。構成の選択肢については前回の記事を参考にしてください。

Cucumberのインストール


今回選択した環境は Cucumber + Capybara + Culerity + Rspec + Spork です。さっそくセットアップしていきましょう。

#gemのインストール
sudo gem install cucumber cucumber-rails celerity
# Culerityで使用するjrubyのインストール
sudo apt-get install jruby 
# JrubyのgemsにCelerityを追加
sudo  /usr/lib/jruby1.2/bin/gem install celerity



サンプルプロジェクトの作成


RSSのFeedを登録するだけのアプリケーションを作ってみます。
本編と関係ありませんが、rails projectの名前にsampleという接頭辞をつけておくと大変便利です。新しく機能を導入する際に一回使いきりのプロジェクトを用意して思い切り遊べるようにできるからです。いらなくなったらプロジェクトはrm sample* -rで、DBはdrop database samplexxxx_test; で削除すれば良いので間違いが少なくて済みます。
$ rails -d mysql samplecucmber
$ ruby script/generate rspec_scaffold feed url:string
:
create  db/migrate
create  db/migrate/20100228205558_create_feeds.rb
route  map.resources :feeds

$ rake db:migrate



rails環境のセットアップ


まずは--capybara, --rspecオプションだけをつけた状態で実行します。
$ ruby script/generate cucumber --capybara --rspec
#設定ファイル。出力先としてreturn.txtが設定されています。
create  config/cucumber.yml 
#同じく設定ファイルですがCucumber実行時のRails環境の動作と必要なconfig.gemを指定しています
create  config/environments/cucumber.rb 
create  script/cucumber #cucumber本体の呼び出しスクリプト
create  features/step_definitions
create  features/step_definitions/web_steps.rb
create  features/support
create  features/support/env.rb
#ここにGherkinで書かれたページ名をURLにマップを設定します。
create  features/support/paths.rb 
create  lib/tasks
#Cucumber関係のrakeタスクを定義
create  lib/tasks/cucumber.rake 

太字は一番最初のcucumberで扱ったプレーンなCucumber環境と比べて増えたファイルです。

次に--sporkオプションをつけてsporkの設定に必要なファイルを見てみましょう。
$  ruby script/generate cucumber --spork --force
overwrite config/cucumber.yml? (enter "h" for help) [Ynaqdh] Y
#DRbが出力に追加されました
force  config/cucumber.yml 
overwrite config/environments/cucumber.rb? (enter "h" for help) [Ynaqdh] Y
#config.gemsporkが追加されました
force  config/environments/cucumber.rb 
identical  script/cucumber
exists  features/step_definitions
identical  features/step_definitions/web_steps.rb
exists  features/support
overwrite features/support/env.rb? (enter "h" for help) [Ynaqdh] Y                      
#require 'spork'が追加されました。requireの記述がSpork.preforkブロックに囲まれました。  
force  features/support/env.rb 
identical  features/support/paths.rb
exists  lib/tasks
identical  lib/tasks/cucumber.rake




次にプロジェクト依存のgemのバージョンをインストールします。この方法で別途gemをインストールするのはgemのバージョンとプロジェクトをそろえて何かの時(人にコードを渡した、しばらくぶりにプロジェクトが再開された)場合にテストを止めないようにするための工夫ではないかと思います。RAILS_ROOT/config/envioronments/cucumber.rbに以下のような記述がありました。どうもここから必要なgemをインストールしている様です。
参考 Ruby on Rails Tips - rake gems:install

#当環境のバージョンは以下のとおり。
#cucumber (0.6.2)
#cucumber-rails (0.2.4)

config.gem 'cucumber-rails',   :lib => false, :version => '>=0.2.4' unless File.directory?(File.join(Rails.root, 'vendor/plugins/cucumber-rails'))
config.gem 'database_cleaner', :lib => false, :version => '>=0.4.3' unless File.directory?(File.join(Rails.root, 'vendor/plugins/database_cleaner'))
config.gem 'capybara',         :lib => false, :version => '>=0.3.0' unless File.directory?(File.join(Rails.root, 'vendor/plugins/capybara'))
config.gem 'rspec',            :lib => false, :version => '>=1.3.0' unless File.directory?(File.join(Rails.root, 'vendor/plugins/rspec'))
config.gem 'rspec-rails',      :lib => false, :version => '>=1.3.2' unless File.directory?(File.join(Rails.root, 'vendor/plugins/rspec-rails'))

config.gem 'spork',            :lib => false, :version => '>=0.7.5' unless File.directory?(File.join(Rails.root, 'vendor/plugins/spork'))


プロジェクト依存のgemをインストールします。
#先に必要なライブラリをインストール
sudo aptitude install libxslt1-dev libxml2-dev

RAILS_ENV=cucumber rake gems:install

このステップでcapybaraがインストールされるはずです。同時に多くの依存するgemがインストールされるので、gemが依存するライブラリがインストールされていない場合はメッセージを出力してcapybaraのインストールが失敗します。該当ライブラリをインストールしてください。


Cucumberを使ったBDD


featureの作成

#feature 'controllerの名前' :オブジェクト変数(script/generate scaffold で指定するのと一緒)
$ ruby script/generate feature feed url:string
exists  features/step_definitions
create  features/manage_feeds.feature
create  features/step_definitions/feeds_steps.rb



それでは試験を開始します。
#sporkサーバを立ち上げます。
$ ~/.gem/ruby/1.8/bin/spork cuc

#drbを設定
$ rake cucumber --drb

#試験実行
$ rake cucumber
(in /home/suzukimilanpaak/workspace/ror/samplecucumber)
/opt/ruby-enterprise-1.8.7/bin/ruby -I "/opt/ruby-enterprise-1.8.7/lib/ruby/gems/1.8/gems/cucumber-0.6.2/lib:lib" "/opt/ruby-enterprise-1.8.7/lib/ruby/gems/1.8/gems/cucumber-0.6.2/bin/cucumber"  --profile default
Using the default profile...
Disabling profiles...
Feature: Manage feeds
In order to [goal]
[stakeholder]
wants [behaviour]

# Rails generates Delete links that use Javascript to pop up a confirmation
# dialog and then do a HTTP POST request (emulated DELETE request).
#
# Capybara must use Culerity or Selenium2 (webdriver) when pages rely on
# Javascript events. Only Culerity supports confirmation dialogs.
#
# cucumber-rails will turn off transactions for scenarios tagged with
# @selenium, @culerity, @javascript or @no-txn and clean the database with
# DatabaseCleaner after the scenario has finished. This is to prevent data
# from leaking into the next scenario.
#
# Culerity has some performance overhead, and there are two alternatives to using
# Culerity:
#
# a) You can remove the @culerity tag and run everything in-process, but then you
# also have to modify your views to use button instead: http://github.com/jnicklas/capybara/issues#issue/12
#
# b) Replace the @culerity tag with @emulate_rails_javascript. This will detect
# the onclick javascript and emulate its behaviour without a real Javascript
# interpreter.
#
@culerity
Scenario: Delete feed                    # features/manage_feeds.feature:34
Given the following feeds:             # features/step_definitions/feed_steps.rb:1
| url   |
| url 1 |
| url 2 |
| url 3 |
| url 4 |
http://www.example.com/feeds
When I delete the 3rd feed             # features/step_definitions/feed_steps.rb:5
Then I should see the following feeds: # features/step_definitions/feed_steps.rb:13
| Url   |
| url 1 |
| url 2 |
| url 4 |

1 scenario (1 passed)
3 steps (3 passed)
0m14.669s


どうでしょううまく通りましたでしょうか?実行エラーはSporkサーバのプロセスの方に吐き出されますので注意してください。これはテストコードがテストを実行しているインタプリタではなくSporkサーバに移譲されているためです。


テストの解説


テストの内容について少し解説します。features/manage_feeds.featureを覗いてみましょう。

このファイルはrake script/generate featureによって自動作成されました。"5つのurlが与えられていて、url='3'のレコードを削除したとき、その他のレコードが残っている"というシナリオです。

Givenのステップで| Url x |として与えられている値はfeatures/feed_steps.rbに配列として渡されレコードがインサートされます。
Given /^the following feeds:$/ do |feeds|
Feed.create!(feeds.hashes)
end


Then句でも||内の値は同様に配列として渡され、htmlの出力結果と異なることを試験します。
Then /^I should see the following feeds:$/ do |expected_feeds_table|
expected_feeds_table.diff!(tableish('table tr', 'td,th'))
end



さあ、これで一応cucumberが動くようになりました。GherkinやWhen-Then-Givenの書き方、ヘルパーメソッドなどに集中できる環境が整いました。次回以降また触れたいと思います。

.