2014年5月18日日曜日

git で作業を取り消したい!

gitでは作業がいろいろな方法で取り消せる。

以前ざっと調べたときにはそういうイメージを持ったのですが、
今回間違えてcommitしてしまって、
さらにpushまでしてしまい、
どうしたものか?と調べたことのまとめ。

結果的には、リモートまで行ってしまったものは『なかったことにできない』という理解。

reset について

基本的には、ローカルでの変更をなしにしよう、というときに使う。
3つの状態を整理しておく。
状態としては、ワーキングツリー、インデックス、HEADがある。
ワーキングツリーは、いま操作しているファイル、目の前にあるファイルの状態のこと。
インデックスは、commitするためにaddされているファイルのこと。
HEADは、ローカルリポジトリの最新ファイルのこと。

git resetは、上述した3つをどうするか?によってオプションが変わる。

$ git reset --soft HEAD^

ワーキングツリー、インデックスは変えずに、HEADの位置だけHEAD^に移動する。

$ git reset --hard HEAD^

ワーキングツリー、インデックス、HEADの位置すべてをHEAD^に移動する。

$ git reset --mixed HEAD^

ワーキングツリーを変えずに、インデックス、HEADの位置をHEAD^に移動する。

ややこしい。

rebase について

まず、rebaseの基本的な使い方を整理する。

rebaseは別ブランチの変更(commit)をいまいるブランチに取り込む方法の一つで、
mergeみたいにいまいるところの「後ろ」に加えるのではなく、
「前」に加える操作。もちろん、一番最初にというわけではなく、
別ブランチと共通の祖先、つまり分岐した部分にうまく追加する。

これをいまいるブランチの過去に対して実行すると、commitを選択して再commitできるようになる。つまり、そのとき、あるcommitを消してしまえばなかったことにできる、ということ。

$ git rebase HEAD~3

ただ、気をつけなくちゃいけないのは、すでに"push"してしまっていると、これ、リモートリポジトリに反映できないです。
ローカルではうまくいくので、しめしめ、と思っているとハマるので注意。

revert について

こちらも実行してしまったcommitをなかったことにするが、ログにちゃんと残る形で実行する。
そうすると、じつはうれしいことがあって、リモートリポジトリもちゃんと変更できる。

$ git revert HEAD

ただし、これも注意点があって。
このコマンドは一つのcommitを打ち消す、というのが機能。
なので、複数ある場合は一度に消されない。


2014年5月11日日曜日

bloggerの引用(blockquote)をかっこよくする!

引用として書いてるのに、なにか味気ない。。
ということで、以下をcssに追加してかっこよくしました。

blockquote {
    background-color : aliceblue;
    border-style : dotted;
    border-color : lightskyblue;
    border-width : 0.1em;
    padding : 1em 1em 1em 3em;
    position : relative;
}
blockquote:before {
    content: "\201C";
    font-family: 'Times New Roman' ,"MS Pゴシック" ,sans-serif;
    font-size : 500%;
    line-height : 1em;
    color : lightskyblue;
    position : absolute;
    left : 0;
    top : 0;
}

ローマは一日して成らず

ちなみに、参考URLは以下です。

content:に指定した"\201C"は秀逸。しばらく文字化けと格闘したので。

gitのmergeとrebaseを考える - その2

昨日も少し書いたけど、いまのところrebaseの使い方は以下のようにしようと思っています。

  • 個人的なブランチをきったときに、メインブランチからの取り込みを簡易に済ませたい
  • メインブランチへのマージのときには使わない


ほんとのところ、どういった使い方を想定してるんでしょう?
参考に以下のページを読んでみました。
Git - リベース

リベース後と元のブランチは確かに同じだと思うのですが、個人的に作業したコミットの履歴を正確には追えなくなると思うんですよね。
まぁ、追う必要がないと言ってしまえばそうなのかもしれないけど。

あと、リベースしてからのマージの利点は「メインブランチへのマージのとき、fast-forwardのみでできる」ということですよね。

うーん、思想の問題で、どっちでもいいような気がしていた。難しいなぁ。

あ、ひとつ、リベースしないでメインブランチにマージすることの利点を思いつきました。
個人的なブランチの分岐点が簡単にわかりますね。そうするとどういった背景で開発してたのかがわかりやすい。

最後までさっきのページを読んでわかりました。
そう、ちゃんと注意書きされている。。(^^;

リモートリポジトリに上げてしまったコミットを変更するようなリベースはしないこと!

てことで、個人的なブランチに取り込む以外は使わない。
うん、やっぱり最初に書いた方針で行こう。

git のmergeとrebaseを考える

いま会社では二人だけの小さなプロジェクトでgitを使っている。

で、僕が管理者なので、もう一人からpull request的なお知らせが来て、
変更したコードを確認して(いわゆるコードレビュー)、
で、その変更を取り込む、という流れな訳だけど。

gitを使い始めた当初はrebaseを使って取り込むようにしていた。
なぜかというと、mergeコマンドの動作を勘違いしていたから、です。

rebaseしてからmergeする

簡単にいうと、修正していたブランチに本筋のブランチ(ここではmaster)の修正点をまず取り込んでおいてから、本筋のブランチにマージする、というやり方。

こんな状態だったとする

例えば、まず以下のブランチがあったとする。
$ git branch
  branch_fix_A
* master

それぞれのログは以下とする。
$ git checkout master
$ git log --graph --oneline --decorate
* 8d03efb (HEAD master) add B.
* 2dcd836 (tag: v_1.0.0) initial commit.
$ git checkout branch_fix_A
$ git log --graph --oneline --decorate
* ac292d9 (HEAD, branch_fix_A) fix A1.
* 5a7c805 fix A.
* 2dcd836 (tag: v_1.0.0) initial commit.
つまり、branch_fix_Aはmasterのタグv_1.0.0からブランチを作成して、
二つのコミットをした状態。
masterの方は、その後に一つのコミットをした状態。

branch_fix_Aをmasterに取り込もうとしている、という状況。

ここでまず、branch_fix_Aでmasterをrebaseする

$ git rebase -i master
そうすると、branch_fix_Aで行ったコミットを取り込むのか、すっ飛ばすのか、などなどを選べる。
pick 5a7c805 fix A.
pick ac292d9 fix A1.
# Rebase 8d03efb..ac292d9 onto 8d03efb
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
今回はこのまま取り込む。

・・・おっと、同じファイルを編集してたので、conflictしてるよ。
error: could not apply 5a7c805... fix A.
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

Could not apply 5a7c80521adfa971cdda95b942cbf962f83e7419... fix A.
てことで、conflictを解消してrebaseを継続する。
$ git add readme.txt
$ git rebase --continue
現状のログを見ると以下となる。
$ git log --graph --oneline --decorate
* 67b268c (HEAD, branch_fix_A) fix A1.
* ef0bb01 fix A.
* 8d03efb (master) add B.
* 2dcd836 (tag: v_1.0.0) initial commit.

で、masterに戻ってmerge

$ git checkout master
$ git merge --no-ff branch_fix_A
Merge made by the 'recursive' strategy.
readme.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
でOK。

$ git log --graph --oneline --decorate
*   f639397 (HEAD, master) Merge branch 'branch_fix_A'
|\ 
| * 67b268c (branch_fix_A) fix A1.
| * ef0bb01 fix A.
|/
* 8d03efb add B.
* 2dcd836 (tag: v_1.0.0) initial commit.
こんな感じで、取り込んだコミットだけ見れて、masterブランチから見て、どのタイミングで取り込んだのかがわかっていいかなぁと思ってたのですが。

実はこれ、branch_fix_Aの二つのコミットのIDが変わっちゃってます。
つまり、修正したときの元の完全な状態に戻せない、ということですね。。

それはまずいかなぁと思って、昨日から(^^; 普通にマージするようにしました。

単純にmergeする

上述と同じ状況でmasterでmergeするだけ

$ git checkout master
$ git merge --no-ff branch_fix_A
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
て、conflictするので解消してcommitする。
$ git commit -m "fix confilict on merge with branch_fix_A."
$ git log --graph --oneline --decorate
*   3aa835d (HEAD, master) fix confilict on merge with branch_fix_A.
|\
| * ac292d9 (branch_fix_A) fix A1.
| * 5a7c805 fix A.
* | 8d03efb add B.
|/
* 2dcd836 (tag: v_1.0.0) initial commit.
これがすっきりしててよいのでしょうね。


で、git mergeのなにを勘違いしていたのかというと、
マージしたい二つのソースがあったときに、手動で差分をとってソースを取り込む、という動作を想像してしまったんです。

えっと、ややこしいですね。。

ブランチが分岐した後のmasterに入ったコミットが、マージのときにややこしくなるんじゃないか?と思ってたんですよね。

人間が手動でやる場合には上述のことを考慮しなくちゃいけないけど、
gitはブランチの分岐点を知ってるので、差分だけをマージしてくれるのです!
もちろんコンフリクトするソースは手動で直すのですが。


てことで、ソース取り込みも手間が少なくなりました。


git で空ディレクトリの登録するには

なかなか手が動いてないけど、仕事ではgitを快調に使用中。

いろいろな場面に遭遇して、あれ?これどうするんだろう?ということばかり。
まだわからないこともあるけど、簡単なところからメモ。

さて、今回は、空のディレクトリを登録する方法。

最初、気づいてなかった。。空のディレクトリを登録してくれないって。
cloneし直してmake通らないじゃん、てなったので気づいた。

空のディレクトリを登録しないのは、gitの仕様らしい。
なので、サイズのないファイルを配置して登録、というのが慣習のようだ。

空のディレクトリの登録

例えば、ファイルが入っていない空のdir_aディレクトリを登録したい場合、以下のように".gitkeep"ファイルを作成しておく。
$ mkdir dir_a
$ touch dir_a/.gitkeep

で、それをコミットすれば登録される。
$ git add dir_a/.gitkeep
$ git commit

まぁ、これはそういうものだと思うしかないね。
バージョン管理システムごとに色合いがあっておもしろい。

CVSの場合は、登録したディレクトリは消せなくなってしまう。
subversionの場合は、ちゃんと管理してくれる。

そう考えると、なにか設計上の思想があるのかな?