たのしいGit
序
言うまでもないことだが、タイトルはジョークである。
そもそもバージョン管理は本来我々がしたい事ではない(一部の人を除く)。別に作りたいものがあり、そこでの作業を円滑に進めるためにバージョン管理するのだから、所詮はヤクの毛刈りである。さらに、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はブランチのコストが小さいため、頻繁に利用できるし、する事が推奨される。「トピックブランチ」はその代表だろう。
というわけで、あるブランチにコミットするという行為は、内部的には、
- リポジトリに現在のbranch headを親とするコミットを追加
- 当該ブランチのbranch head情報を更新
という手続きになる。
- git-branch(1)
Tag
タグはコミットへのラベルである。タグはリリース時など、ある特定のコミットを指示したい場合に用いる。タグには署名をつけることも出来る。
ブランチとの違いは、タグ付け後に新たなコミットがあっても、タグは同じコミットを指し続ける点である。
- git-tag(1)
Remote
公開されているリポジトリを手元のリポジトリに取り込んだり、手元のリポジトリを公開する場合は、まずその外部リポジトリの場所をremoteに登録し、それとの操作として扱う。
upstreamという名前のremoteを登録
git remote add upstream git://github.com/ruby/ruby.gitupstreamという名前のremoteから実際にデータをとってくる
git fetch upstreamgit-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を行う。
- トピックブランチを作る
- 変更する
- トピックブランチをどこかのremoteにpush
- 取り込んでもらいたい相手に変更をpullしてほしいと、remoteのurlを教える
特にGithubのpull requestの場合は、以下の通りになる。
- トピックブランチを作る
- 変更する
- トピックブランチをGithubにpush
- GithubのWebページからpull requestボタンを押し、相手にrequestを送る
GithubのWebだけで操作することもできる。
- 本家からfork
- 編集したいファイルをEdit
- 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は手元に本家の変更を随時取り込む形にしたいときに使う。
- 追従させたいブランチをcheckout git checkout local
- 追従先に対してrebase git rebase origin/master
- 手元のコミットが追従先と衝突した場合は二種類から選ぶ。
- 手元のコミットを修正して使う場合は、修正し、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 -d か git branch -D。
リモートブランチ
git push <remote> :<branch name> コロンを付けるとか意味わからないよね…。
Notes
-
biscota reblogged this from takepara
-
ot2sy39 liked this
-
tatsuhico reblogged this from nalsh
-
tokujoushibire reblogged this from syoichi
-
mpixy reblogged this from nalsh
-
wate-wate reblogged this from nalsh
-
nloc reblogged this from nalsh
-
nysta reblogged this from nalsh
-
skoei reblogged this from nalsh
-
amattresswarehouse reblogged this from nalsh
-
yashimanote reblogged this from nalsh
-
soramugi liked this
-
katakori reblogged this from nalsh
-
goonew liked this
-
passingloop liked this
-
ukar reblogged this from saitamanodoruji
-
no-theme reblogged this from syoichi
-
umbrellaprocess liked this
-
keidou liked this
-
mostlyfine reblogged this from nalsh
-
ishiduca liked this
-
sky-y liked this
-
mug-g reblogged this from nalsh
-
criff reblogged this from saitamanodoruji
-
knives777 reblogged this from saitamanodoruji
-
takepara reblogged this from nalsh and added:
ヤクの毛刈り...。
-
oshirini-yasashiku liked this
-
rousseau reblogged this from ss846
-
inatomi liked this
-
rtfgbhu reblogged this from syoichi
-
qawsklp liked this
-
ss846 reblogged this from saitamanodoruji
-
ume22 reblogged this from saitamanodoruji
-
kubotomo reblogged this from saitamanodoruji
-
saitamanodoruji reblogged this from nalsh
-
hsshkmn reblogged this from syoichi
-
takurot reblogged this from nalsh
-
nobby0-0 reblogged this from syoichi
-
kai-app reblogged this from nalsh
-
manjiro reblogged this from nalsh
-
ystream liked this
-
peccu liked this
-
peccu reblogged this from nalsh
-
youandi051024 reblogged this from nalsh
-
yoderkeez reblogged this from nalsh
-
knnr reblogged this from syoichi
-
iwadon reblogged this from nalsh
-
ayu reblogged this from syoichi
-
jumpeing liked this
-
takashi-u liked this
- Show more notes