gitを使用するようになってから、数年。自分なりの使用方法が定着してから、結構経つが、最近、その使用方法の改善について、停滞気味。
gitは、あることをするのに色々な手段が取れる。だから、より簡単で安全で効率の良い方法をいつも探している。
それ故に、初心者やSubversionからの移行者が非常に取っつきにくく、調べても、基本的な手法すら記載されていない。調べても出てこないので、それが正しいのか、どうすると効率が良いのかなど、私も未だ悩むことも多い。
大体、gitの使用方法を調べていると、下記の2段階で出てくる。
- gitの基本的なコマンド
- 運用方法(GitHubフローを使うとか)
しかし、Subverionと違い、色々な方法が取れるgitであるからこそ、1と2の間にあたるユーザー操作の方法が無限に存在するように思っている。
私がSubversionから移行するときも、その部分には悩まされたし、プロジェクトメンバーに教えていても、コマンドは似通った部分があるから何となく分かるけど、何が正解か分からない、ベストプラクティスがないどころか、基本的な手法すら出てこないとよく聞く。
状況によって、ベストな方法があるから、とも言えるかもしれないが、それでもとりあえずは操作の指標は欲しいところ。
つまり、実際に書いたコードを上げたり、他人のコードを取り込んだりする時、どのやり方(pull、fetch & merge、rebase)をすれば正解か、実際にやった時にどんな挙動になるかもよくわからないし、それがあってるかどうかも分からないから、困ってるということ。
まずは、ブランチ
Subversionからgitへ移行する際、ブランチの取り扱いの差異に最初は戸惑った。Subversionのブランチは運用手法だが、gitでは、システムだ。
gitのブランチは他人へ影響が無いから、とにかく簡単にブランチが切れる。影響が無いし、簡単に戻せる。
gitは、ブランチを操作するものだから、まずはブランチを理解する必要がある。
二種類のブランチ
ともかく、まず、ローカルリポジトリ内には、リモートブランチとローカルのブランチがあることを理解すること。
コマンドで見てみると、こんな感じになってる。
$ git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
これは、全部ローカル内にある。先頭にremotesと付いてるのが、外部のリポジトリと連携をとるためのもので、外から持ってきたものが入ってる。そして外に出す際に一緒に更新される。どの順番で更新されるのかは知らない。要は、外部のコピーもローカルに入っていると思っておけば良い。
詳しくは、下記を見ていただいた方が良いかと思う。
http://qiita.com/uasi/items/69368c17c79e99aaddbf
操作方法
で、それぞれの操作方法は下記の通り。remotesの中身は、勝手に更新されないので、fetchで手動更新する。ある程度操作したことがあるなら、これで何となくデータの流れがわかってくると思う。
操作内容 | ローカルブランチ | リモートブランチ |
---|---|---|
作成 | checkout -b | push |
更新(自分の) | commit | push |
更新(取り込み) | merge | fetch |
削除 | branch -d | push --delete |
自動連係
push、pullする時はupstreamが設定してあれば、push先は指定しなくても良い。masterしか使用していないと、最初に自動設定されただけで触らないので分からないと思うが、要はローカルブランチとリモートブランチの関連付けを行っておく。ブランチ名は同じである必要はないので、自分で好きなように関連付けられる。
下記のようになっている場合、hogeは連携先がないので、pullやpushする際に宛先を指定する必要がある。宛先は、origin/hogeにも、origin/masterにもできる。masterをorigin/hogeに関連づけることもできる。
$ git branch -vv
* master aaaaaa [origin/master] test
hoge aaaaaa test
ローカルにあるものは自分の自由にしてよい
ローカルにあるものは、壊してしまっても戻してしまえば良い。使わないのなら、ローカルのmasterブランチも消しても全く問題ない。所詮、master
と名のついた只のブランチでしかないから。
ただし、remotes/のブランチは、コマンド操作すると相手側と連動されるので、ローカルから消したくなったらgit remote remove origin
で、連携を外すか、.gitディレクトリごと消そうw
まずはブランチを理解すること
- ブランチ同士にどのような関連性があるか(ローカルとremotes)
- どうやって関連づいているか(upstreamの設定)
- どのようなデータフローになっているか(fetch、merge、pushした時にどこが更新されるか)
gitを操作しようとすると、このブランチが理解できずに行き詰まる。使用するだけなら、それほど難しくないはずなので、あんまり深く考えず、感じてみよう。
私が使用しているgit操作手順
で、色々彷徨いつつ、自分なりに確立した方法が下記になる。
- ローカルリポジトリを共有リポジトリからcloneする(
git clone URL
) - 作業ブランチを切る(
git checkout -b feature_featurename_myname
) - 作業する
- 一日の終わり、または気が向いたときにcommitする(
git add . && git commit -m 'f'
) - 一区切りついたら、commitを整理する(
git rebase -i HEAD^^^
) - リモートのcommitを取り込む(
git remote update && git fetch -p && git rebase origin/master
) - conflictが起きたら、解消する(
git mergetool
) - よくわからなくなったら、戻す(
git rebase --abort
) - merge失敗した時用にバックアップを取っておく(
git checkout -b backup
) - マージできたら次を処理する(
git rebase --continue
) - 全体のdiffを見て、きちんとmergeできたか判断する(
git diff backup feature_featurename_myname
) - バックアップも兼ねて、リモートへブランチをpushする(
git push -f origin feature_featurename_myname
) - 3 - 7 を繰り返す
- 機能が完成したら、masterにマージする(
git checkout master && git fetch -p && git pull && git merge --no-ff feature_featurename_myname
) - マージしたmasterをremoteに送る(
git push origin master
) - 不要になったブランチを消す(
git branch -d feature_featurename_myname && git push --delete origin feature_featurename_myname
)
プロジェクトメンバーのgit練度の兼ね合いもあって、GitHubフローなどは使用していないが、とりあえず、この操作内容の意味が分かるのであれば、運用方法が何になっても困らないと思う。
ちょっと補足
-
git remote update
は、リモートが複数ある時だけで良い - ローカルmasterの更新は、pullでも、mergeでも良い
- 変更点取り込みのタイミングは、固定ではない、commitしてすぐにしておいても良い。
利点
この方法だと、commitが綺麗になる。masterの取り込みをrebaseで行って(git rebase origin/master
)、ブランチの適用をnon fast-forward(git merge --no-ff
)で行うと、他の人の変更点を取り込んだ後で、自分がまとめて変更点を適用したことになる。
時系列的には違うが、コードの正当性としては(conflictをきちんと解消していれば)間違っていない。
また、ブランチでの作業分が、まとまってきっちり分離され、概要がmergeコメントに載っているので、非常に分かりやすい。
欠点
git rebaseでconflictが起きた場合、解消が難しくなることがある。rebaseの間、ずっとconflictが起きることがあるため。
それぞれの意味
commitする
無駄にcommitしてるけど、大丈夫か。問題ない。
後にも書くが、commit時点では確定ではない。むしろ、訳の分からない変更点が大量に貯まる前に、適当な位置でcommitしておいた方が、前回作業とのdiffも取れて便利。
commitが増えてきたら、とりあえず、fixupでまとめてしまえば良い。
git rebase origin/master、git mergetool
利点にも書いたが、rebaseすると、最新の状態から、自分が変更を行ったことになる。
その作業で、conflictをきちんと解消するはずなので、それで問題ないはず。問題になったら、きちんとconflictを取り込めなかった自分が問題。それは、相手の変更点を理解していなかったと同義だ。
また、mergeで取り込むと、自分のコミットログと他人のコミットログが混ざるが、rebaseは、当たり前だが、それがない。
rebase origin/master
は、下記のようにイメージすると分かりやすい。
- 最新のorigin/masterの先頭に自分のcommitを順番に先頭にくっつけていく
- conflictが起きたら、その時点の変更ファイルをワーキングに展開して止まる
- conflictを解消したら、
git rebase --continue
すると、そこからまた、自分のcommitを順番にくっつけていく
変更ファイルを展開する時は、また、git branch
を見てみると分かるが、一時ブランチで行われるので、git rebase --abort
で、rebase実行直前の状態にすぐに戻せる。
git push -f origin feature_featurename_myname
push -f
はいけないんじゃ。。と思うかもしれないが、自分専用のブランチで問題なんて起きるの?
間違って、masterにpush -f
する可能性が問題かもね。。まあ、それをやらかすのは、masterの過去のcommitを変更するような使用方法に問題があるからだけどね。普通は無いね。
git pull && git merge --no-ff feature_featurename_myname
masterは、リモートと同期をとっているだけなので、作業ブランチと違いgit pull
してもコミットログが崩れない。
利点にも書いたが、--no-ff
をつけて、ブランチを取り込むことで、自分の作業分のみを分離したログを出せて、見やすくなる。
Subversionとの主な違い
- とりあえず、commitしまくる。途中でもする。Subversionのようにcommitを神聖視するな。(masterにpushしなければ)何も問題にならない。日常のcommitは、らくがき程度の扱いなので、pushする前に、他人に見せるようにまとめれば良い。
- ローカルのリポジトリや自分専用のブランチは、他人が触る余地がないので、都合が良いように改変しまくれ。誰も見てないんだから(masterにpushしなければ)何も問題ない。そして、ブランチも切りまくれ。困ったら切れ、不安なら切れ、ブランチは、超低コストの
cp -r
+vimdiff
だ。ブランチを切った(所謂cp -r
した)先は、壊しても問題ない。元のブランチを使うだけだ。うまくいったらmergeで取り込む(vimdiff)なり、そっちをメイン作業スペースにするだけだ。
Subversionからすると、commitやブランチの扱いがだいぶ違うと思う。Subversionからの認識を改めるべき。
Subversionとgitの対応コマンドを調べるとよく見つかる。確かにその通り間違っていないのだが、そのまま使用しようとするとgitの利点を全く生かせない。それどころか、無理にSubversionに合わせようとするせいで混乱が生じて、余計に難しくなってしまう。
特に、commit、svn commit = git commit && git push
とは思うな、svn commit = git push origin master
と思うべき。ここでのorigin master
は重要。
git commit
は、確定された作業内容ではなく、個人の作業ログ程度の扱いで良い。
そして、Subversionでは、手動で行っていた操作をgitで簡単に行える。
例えば、Subversionでは、mergeするとワーキングツリーが壊れるから。。とかで、ファイルやワーキングツリー丸ごと、一時的な退避を行っていたが、それもgitでは、reflogで大体戻せるし、バックアップ用ブランチを切ってしまえば、それらを手動でやる必要すらない。
対応表に表すとこんな感じだと思えば良い。とにかく、目的は、大体gitで解決できるはず。
Subversion | Git | 意味 |
---|---|---|
svn commit | git push origin master | 変更確定 |
git commit | 今日の作業、気が向いた時の変更ログ、変更確定の準備 | |
git checkout -b feature | 作業スペース確保 | |
cp -r ../project{,.bak} | git checkout -b backup | バックアップ、変更テスト用スペース |
cp file{,.bak} | git commit -m 'f' | 確度の高い変更箇所を残す |
cp file{,.bak} | git stash | 確度の低い変更点をバックアップ |
svn up | git rebase origin/master | 変更点の取り込み |
最後に
gitシステム自体が、ブランチを使用しているところが多々あるように、gitは、ブランチを操作するものである。だから、ブランチの扱いに慣れることが重要。
それと、Subversionからの移行者は、commitやブランチの認識を改める必要がある。