1. wordijp

    Posted

    wordijp
Changes in title
+Gitで今のブランチ分岐点を取得するalias/git-stash-commitというサブコマンドをRubyで作った
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,220 @@
+この記事は二本立てです、どちらをAdvent Calendarネタにしようか迷ったんですが、どうせならという事で両方書く事にしました。
+
+
+# Gitで今のブランチ分岐点を取得するalias
+
+git rebase -i時に、ブランチ分岐点までを対象にしたいって時がよくあります。
+
+```
+ develop
+--- * --- *
+ \ <-rebase -iしたい->
+ * --- * --- * --- * --- * --- *
+ \ topic(HEAD)
+ *
+ topic-foo
+```
+
+HEAD^NでNを数えるのも大変なので、分岐点を手軽に取得出来たら良いなという事で作ったのがこちらです。
+
+```:.gitconfig
+[alias]
+ ; git show-branchの---より後ろを表示
+ ; NOTE : 空行(/^$/)はヒットしない(/^-/,/^$/で---以降を表示)
+ show-branch-body = "!f() { \n\
+ git show-branch $1 | gawk '/^-/,/^$/ {print}' | tail -n +2 \n\
+ }; f"
+
+ ; 分岐点を取得
+ ; NOTE : 自身(*)以外が登場すると分岐点
+ divide-base = "!f() { \n\
+ git show-branch-body $1 | \n\
+ gawk -F'[' 'match($1, /*/) && match($1, /[^ *]/) {print; exit}' | \n\
+ sed -e 's/^.\\+\\[\\(.\\+\\)\\].*/\\1/' | \n\
+ tr -d '\n' \n\
+ }; f"
+
+ ; HEADから分岐点までrebase -i
+ rebase-i-divide-base = !git rebase -i $(git divide-base)
+ ridb = !git rebase-i-divide-base
+```
+
+
+## 参考
+
+[親ブランチ(ブランチ分岐点)を確認](http://yanor.net/wiki/?Git%2Fgit%20branch%2F%E8%A6%AA%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%EF%BC%88%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%E5%88%86%E5%B2%90%E7%82%B9%EF%BC%89%E3%82%92%E7%A2%BA%E8%AA%8D)
+
+
+# git-stash-commitというサブコマンドをRubyで作った
+
+現在の作業内容の退避にはgit stashコマンドの出番ですが、このコマンドが有効なのはせいぜい一つの作業中断までで、三つ以上の同時進行時はstashコミットがごちゃ混ぜになってしまいます。
+wipブランチにコミットするのも良いですが、ブランチ別のstashを手軽に扱いたかったのでサブコマンドとして作りました。
+
+[wordijp/git-stash-commit(GitHub)](https://github.com/wordijp/git-stash-commit)
+gemで提供してますので、gemコマンドでインストールします。
+
+ $ gem install git-stash-commit
+
+コマンド一覧はhelpオプションで見れます。
+
+ $ git stash-commit help
+
+また、CONFLICT時やブランチリネーム時のコーナーケース対応はREADME.mdに記載しています。
+
+## 仕組みについて
+
+git stashのようにtracked filesを別ブランチへ退避します、機能拡張として、ブランチ別/退避ごとのブランチを作るようにし、既存の退避済みプランチへ追加もできるようしてます、なお、このサブコマンドで作成されるブランチは作業用を除き通常のブランチとして作成しています。
+
+内部では次のような処理が行われています。
+
+1. 現在の変更のあるtracked filesを専用ブランチへcommit
+2. 追加の場合は、そこからrebaseする
+3. カレントブランチを戻す
+
+```
+# 何か作業
+$ git stash-commit
+# 何か作業
+$ git stash-commit
+--- * --- * --- * <-- topic(HEAD)
+ |\
+ | * <-- stash-commit/topic@0 (一回目の退避)
+ \
+ * <-- stash-commit/topic@1 (二回目の退避)
+
+# 何か作業
+$ git stash-commit --to 0
+--- * --- * --- * <-- topic(HEAD)
+ |\
+ | * --- * <-- stash-commit/topic@0 (追加の退避)
+ \
+ * <-- stash-commit/topic@1
+
+$ git stash-commit --from 0
+--- * --- * --- * <-- topic(HEAD) (@0の退避内容が戻る)
+ \
+ * <-- stash-commit/topic@1
+
+# ここから複数の続き
+
+# 続き(A)
+$ git checkout -b other
+$ git add -u
+$ git commit -m "other"
+# 何か作業
+$ git stash-commit
+--- * --- * --- * topic
+ |\
+ | * <-- other(HEAD) (topic@0の退避内容がここへ来る)
+ | \
+ | * <-- stash-commit/other@0
+ \
+ * stash-commit/topic@1
+
+
+# 続き(B)
+$ git add -u
+$ git commit -m "commit"
+# 何か作業
+$ git stash-commit
+--- * --- * --- * --- * <-- topic(HEAD) (もとの@0の退避内容がここへ来る)
+ \ \
+ \ * <-- stash-commit/topic@0 (新しいtopic@0)
+ \
+ * stash-commit/topic@1
+
+# 続き(C)
+$ git add -u
+$ git commit -m "commit"
+# 何か作業
+$ git stash-commit --to 1
+--- * --- * --- * --- * <-- topic(HEAD) (@0の退避内容がここへ来る)
+ \
+ * --- * <-- stash-commit/topic@1
+```
+
+## Rubyで作った感想
+
+作りやすかったです、シェルスクリプトのように関数には戻り値の制約もなく、クラスも使えます。
+また、シェルスクリプトでは実装が困難だと思われる機能もRubyでは難なく実装できました。
+
+例えば、stash-commitブランチは通常のブランチなんですが、作業用ブランチは住み分けのためにdetached HEAD状態にしています、しかしこちらはcommitしてリビジョンが変わっても追従してくれません。
+そのため、Branchクラス、DetachBranchクラスを作成してインターフェースを統一し、DetachBranchクラスの場合はブランチの追従処理を追加して通常のブランチと等価な処理を行えるようにしています。
+
+*※説明用にシンプルにしています。*
+
+```rb:branch.rb
+module BranchFactory
+ extend self
+
+ # ブランチ名をもとに、通常/detachedブランチどちらかを判断し、返す
+ def find(name)
+ if 通常ブランチ名? name
+ Branch.new name
+ elsif detached HEAD状態のブランチ名? name
+ DetachBranch.new name
+ end
+ end
+end
+
+module BranchCommon
+ 共通処理
+end
+
+class Branch
+ include BranchCommon
+
+ def commit()
+ `git commit -m "commit"`
+ end
+end
+
+class DetachBranch
+ include BranchCommon
+
+ def commit()
+ `git commit -m "commit"`
+ `git update-ref refs/ブランチ名 HEAD` # ブランチの追従
+ end
+end
+```
+
+
+# おわりに
+
+サブコマンドをRubyで書くのはおすすめです、外部コマンドとの親和性も高いです。
+しかし.gitconfigのaliasをRubyで書くのはやめた方が良いです、次のような理由で書くのが辛かったです。
+
+## .gitconfigにRubyでaliasを書くと全力で邪魔してくる
+
+非常に辛いです、ざっと以下のような制約があります。
+
+- .gitconfigのエスケープと""文字列内のエスケープで倍々ゲーム:video_game:
+- 「"#{value}"」と書くと#以降は.gitconfigのコ・メ・ン・ト:heart:
+- 「"\\043{value}"」?、ああ、そういう文字列なんですね^^
+- そのコメント、うちのなんですよ:sweat:
+- 一行で書いてる?、なんの事?、あ、その「;」コメントですね:smile: ※真面目なだけ、^^ではない
+
+```:.gitconfig
+[alias]
+ zenryoku-bougai = !ruby -e ' \n\ ↓ 説明用
+ \n\
+ # NG comment \n\ <- 構文エラー(.gitconfigのコメント扱い)
+ \"OK comment\" \n\ <- 文字列をコメント代わりに使える
+ \n\
+ puts 123; puts 456 \n\ <- 構文エラー(;以降がコメント)
+ \n\
+ str = \"hello\" \n\
+ puts \"#{str} world\" \n\ <- 構文エラー(#以降がコメント)
+ puts \"\\043{str} world\" \n\ <- 「#{str} world」(展開されない)
+ \n\
+ puts \"\\\\\\\\\\\\\\\\100yen\" \n\ <- これで「\100yen」
+ \n\
+ printf `echo #{str}` \n\ <- 構文エラー(#以降がコメント)
+ printf IO.popen(\"echo \" + str, \"r+\") {|io| \n\ <- OKだが大げさ
+ io.gets \n\
+ } \n\
+ '
+```
+
+aliasは素直にシェルスクリプトで書きましょう。