Edited at
GitDay 14

Gitで今のブランチ分岐点を取得するalias/git-stash-commitというサブコマンドをRubyで作った

More than 1 year has passed since last update.

この記事は二本立てです、どちらを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



応用

ここまでは完璧!ってcommitに何でもいいのでブランチを作成します、例えば「ok」というブランチを作成します。

すると、rebase-i-divide-base時にそこまでがrebase -i対象になります。

--- * --- *

\ <- 対象 ->
* --- * --- * --- * --- * --- *
\ ↑ok topic(HEAD)
*
topic-foo

「git rebase -i ok」でも同じ事なんですが、こちらはブランチ名の事を気にせずに済みます。


参考

親ブランチ(ブランチ分岐点)を確認


git-stash-commitというサブコマンドをRubyで作った

現在の作業内容の退避にはgit stashコマンドの出番ですが、このコマンドが有効なのはせいぜい一つの作業中断までで、三つ以上の同時進行時はstashコミットがごちゃ混ぜになってしまいます。

wipブランチにコミットするのも良いですが、ブランチ別のstashを手軽に扱いたかったのでサブコマンドとして作りました。

wordijp/git-stash-commit(GitHub)

gemで提供してますので、gemコマンドでインストールします。

$ gem install git-stash-commit

コマンド一覧はhelpオプションで見れます。

$ git stash-commit help

usage)
git stash-commit [--to (index | name)] [-m <commit message>] [-a | -p]
options : --to default: unused index
-m | --message default: "WIP on <branch>: <hash> <title>"
-a | --all default
-p | --patch
NOTE : --all equal 'git commit --all'
--patch equal 'git commit --patch'
git stash-commit --from (index | name) [--no-reset]
NOTE : --no-reset rebase only
git stash-commit --continue
git stash-commit --skip
git stash-commit --abort
git stash-commit --rename <oldname> <newname>
NOTE : stash-commit/<oldname>@to stash-commit/<newname>@to
git stash-commit -l [-a]
options : -l | --list listup stash-commit branch, in this group
-a | --all listup stash-commit branch all
git stash-commit help
git stash-commit <any args> [-d]
options : -d | --debug debug mode, show backtrace


仕組みについて

git stashのようにtracked filesを別ブランチへ退避します、機能拡張として、ブランチ別/退避ごとのブランチを作るようにし、既存の退避済みプランチへ追加もできるようしてます、なお、このサブコマンドで作成されるブランチは作業用を除き通常のブランチとして作成しています。

内部では次のような処理が行われています。


  1. 現在の変更のあるtracked filesを専用ブランチへcommit

  2. 追加の場合は、そこからrebaseする

  3. カレントブランチを戻す


使い方について

基本の使い方は「git stash-commit [--to]」で退避して、「git stash-commit --from 退避名」で退避を戻す、です、--to指定の時は追加もできます。


基本オプションの--toと--from

# 何か作業

$ 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


「git stash-commit [--to]」に--patchオプションを付けると部分コミットが出来ます、内部ではcommit --patchが動いています。


--patchオプション

$ git stash-commit --patch

# commit内容を選択
$ git stash-commit
--- * --- * --- * <-- topic(HEAD)
|\
| * <-- stash-commit/topic@0 (--patch内容の退避)
\
* <-- stash-commit/topic@1 (--patch残りの退避)

「git stash-commit [--to]」や「git stash-commit --from」時に、CONFLICTする事があります、その時は手動マージして--continueオプションで継続、--abortでキャンセル、--skipで変更を捨てる、のどれかで解決します。


--continueオプション

$ git stash-commit --to 0

topic
--- * --- * --- * --- * <-- stash-commit/topic@0-progresstmp(作業用ブランチ)
|\
| * <-- stash-commit/topic@backup(作業用ブランチ)
\
* <-- stash-commit/topic@0
# 手動マージ
$ git add CONFLICTファイル
$ git stash-commit --continue
--- * --- * --- * <-- topic(HEAD)
\
* --- * <-- stash-commit/topic@0

ブランチ名を変更した時は、--renameオプションを使います。


--renameオプション

$ git branch -m topic newname

$ git stash-commit --rename topic newname
--- * --- * --- * <-- newname(HEAD)
\
* <-- stash-commit/newname@0

stash-commitブランチ一覧の取得は--listオプションを使います。


--listオプション

$ git stash-commit --list

stash-commit/topic@0
--- * --- * --- * <-- topic(HEAD)
\
* <-- stash-commit/topic@0

全ブランチの時は--allオプションを追加します。


Rubyで作った感想

作りやすかったです、シェルスクリプトのように関数には戻り値の制約もなく、クラスも使えます。

また、シェルスクリプトでは実装が困難だと思われる機能もRubyでは難なく実装できました。

例えば、stash-commitブランチは通常のブランチなんですが、作業用ブランチは住み分けのためにdetached HEAD状態にしています、しかしこちらはcommitしてリビジョンが変わっても追従してくれません。

そのため、Branchクラス、DetachBranchクラスを作成してインターフェースを統一し、DetachBranchクラスの場合はブランチの追従処理を追加して通常のブランチと等価な処理を行えるようにしています。

※説明用にシンプルにしています。


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は素直にシェルスクリプトで書きましょう。