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

この記事は二本立てです、どちらを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」でも同じ事なんですが、こちらはブランチ名の事を気にせずに済みます。


## 参考

[親ブランチ(ブランチ分岐点)を確認](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
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クラスの場合はブランチの追従処理を追加して通常のブランチと等価な処理を行えるようにしています。

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

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