git rebase --interactive でコミットの見分けがつかない問題の対策

# 対策

## `sequence.editor`で`rebase -i`がフックできる

[`sequence.editor`][conf] という設定項目は本来、`rebase --interactive`のTODOを編集するエディタを指定するためのものですが、ここをフックとして使うことでTODOのテキストファイルを加工できます。

こういう設定をgitconfigにします。

```ini:gitconfig
[sequence]
	editor = "f() { perl -i -ne 'BEGIN { $c = qx(git config core.commentchar); chomp $c; $c or $c = q(#) }; print; /^[a-z]+ ([0-9a-f]+)/ or next; $i++; $_ = qq( [$i] ) . qx(git log -1 --stat=128 --dirstat=0 --pretty=format:%h:%s%n%b%N $1); $? and die qq(git log $1 failed); s/^/$c/gm; print qq($_\\n)'  \"$@\" && \"$(git var GIT_EDITOR)\" \"$@\"; }; f"
```

## 出力例

通常`git rebase -i`は、次のようなコミットの一覧(rebase todo listというそうです)をテキストエディタで編集します。 ([元コミット](https://github.com/webmproject/libwebp/compare/95ecccf...2102ccd))

```lisp:素のリスト
pick 95ecccf only apply color-mapping for alpha on the cropped area
pick 31f2b8d WebPAnimEncoder: FlattenSimilarPixels(): look for similar
pick 47dd070 anim_diff: Add an experimental option for max inter-frame diff.
pick 6e12e1e WebPAnimEncoder: Fix for single-frame optimization.
pick e3912d5 WebPAnimEncoder: Restore canvas before evaluating blending possibility.
pick 2102ccd update the Unfilter API in dsp to process one row independently

; Rebase aa809cf..de47492 onto aa809cf (6 commands)
;
; 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
; d, drop = remove commit
;
; 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
```

冒頭の設定をすると、次のように通し番号、diffstat、コミットメッセージが各行に付加されます。

```lisp:加工後
pick 95ecccf only apply color-mapping for alpha on the cropped area
; [1] 95ecccf:only apply color-mapping for alpha on the cropped area
;This is only possible if the filtering is not VERTICAL or GRADIENT.
;Otherwise, we need the spatial predictors and hence need the un-visible
;part above crop_top row.
;
;COLOR_INDEX transform is the only transform that is not predicted
;from previous row. Applying the same for other transform (spatial
;predict, ...) is going to be more involve and use an extra temporary row.
;
;+ remove ApplyInverseTransformsAlpha()
;(work is done directly within ExtractPalettedAlphaRows())
;
;+ change back to using filter_ instead of unfilter_func_
;
;Change-Id: I09e57efae4a4af00bde35f21ca6e3d73b35d7d43
;
; src/dec/alpha.c  | 16 +++++++---------
; src/dec/alphai.h |  2 +-
; src/dec/vp8l.c   | 58 +++++++++++++++++++++++++++++-----------------------------
; 3 files changed, 37 insertions(+), 39 deletions(-)
; 100.0% src/dec/

pick 31f2b8d WebPAnimEncoder: FlattenSimilarPixels(): look for similar
; [2] 31f2b8d:WebPAnimEncoder: FlattenSimilarPixels(): look for similar
;not exactly same.
;
;Based on lossy WebP quality setting, ignore minor differences when
;flattening
;similar blocks.
;
;For 6k set, at default quality with '-min_size' option, improves
;compression by 0.3%
;
;Change-Id: Ifcb64219f941e869eb2643e231220b278aad4cd4
;
; src/mux/anim_encode.c | 55 +++++++++++++++++++++++++++++++++++++++++++------------
; 1 file changed, 43 insertions(+), 12 deletions(-)
; 100.0% src/mux/

pick 47dd070 anim_diff: Add an experimental option for max inter-frame diff.
; [3] 47dd070:anim_diff: Add an experimental option for max inter-frame diff.
;After the introduction of lossy frame rectangles
;we need equivalent option in anim_diff for merging similar frames.
;
;Change-Id: I1d03acace396ec4cb0212586c6e8b8ec5b0b0bfc
;
; examples/Makefile.am |  2 +-
; examples/anim_diff.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
; 2 files changed, 76 insertions(+), 7 deletions(-)
; 100.0% examples/

pick 6e12e1e WebPAnimEncoder: Fix for single-frame optimization.
; [4] 6e12e1e:WebPAnimEncoder: Fix for single-frame optimization.
;Change-Id: I2da8dc34ab9589b58cde0ecb05d71357d77f6b25
;
; src/mux/anim_encode.c | 10 ++++++----
; 1 file changed, 6 insertions(+), 4 deletions(-)
; 100.0% src/mux/

pick e3912d5 WebPAnimEncoder: Restore canvas before evaluating blending possibility.
; [5] e3912d5:WebPAnimEncoder: Restore canvas before evaluating blending possibility.
;Without this, the canvas may have been modified in-between two
;GenerateCandidate() calls.
;
;Change-Id: I0364a269caabdf282c30a8fa3c61896f0247342e
;
; src/mux/anim_encode.c | 8 ++++++--
; 1 file changed, 6 insertions(+), 2 deletions(-)
; 100.0% src/mux/

pick 2102ccd update the Unfilter API in dsp to process one row independently
; [6] 2102ccd:update the Unfilter API in dsp to process one row independently
;This will allow to work in-place on cropped area later.
;
;Also sped up the inverse gradient filtering in SSE2 (~4%)
;
;Change-Id: I463149eee95d36984328f163a1e17f8cabd87441
;
; src/dec/alpha.c               |  31 ++++++++---
; src/dec/alphai.h              |   1 +
; src/dec/vp8i.h                |   1 +
; src/dec/vp8l.c                |  55 ++++++++++--------
; src/dsp/dsp.h                 |   6 +-
; src/dsp/filters.c             |  39 ++++++++++---
; src/dsp/filters_mips_dsp_r2.c | 162 +++++++++++++++++++++---------------------------------
; src/dsp/filters_sse2.c        | 272 +++++++++++++++++++++++++++++++++++++++++-------------------------------------------------
; 8 files changed, 278 insertions(+), 289 deletions(-)
;  16.6% src/dec/
;  83.3% src/dsp/


; Rebase aa809cf..de47492 onto aa809cf (6 commands)
;
; 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
; d, drop = remove commit
;
; 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
```

# 経緯

## push前にコミットを並べ替えたい

```:状態A
A <- devel
|
B
|
C
|
D
|
E <- origin/devel
|
...
```

コミットがたまってきて`push`する前に順番を並べ替えたいことはよくあります。
こんなかんじにAとDをまずくっつけて、その後にBとCを入れたいとします。

```:状態B
C <- devel
|
B
|
[DとA]
|
E <- origin/devel
|
...
```

## 数個ならresetとcherry-pickでいい


2～3個までなら、最後にpushした時点まで`reset`して`cherry-pick`のほうが手っ取り早いです。
以下のコマンドで図Bの通りになります。

```shell-session
$ git reset --hard E # git reset --hard origin/devel でも可
$ git cherry-pick -n D A
$ git commit -m "DとA"
$ git cherry-pick B
$ git cherry-pick C
```

ただ、この方法は6個を超えたあたりからだんだん辛くなってきます。

## 数が多いとrebase -iしか手がない

`git rebase --interactive`は大量の細かいコミットを編集するのに便利で、並べ替え、分割、統合のどれにも使えます。問題があるとすれば…

## コミットの見分けがつかない問題

問題があるとすれば、コミットメッセージをまともに書かない人が使うとTODOの見分けが全くつかないことです。

```shell-session:rebase-todo
$ git rebase -i @~20
pick 97934e2 hoge
pick ec1b240 hoge
pick d96fe5e fix
pick f804008 hoge
pick ff0a94b hoge
pick eb42390 fuga
pick 9a950c5 hoge
pick 8dca024 foo
pick 47dd070 hoge
pick 6e12e1e typo
```

「どうせsquashするしメッセージはあとで書くからいいんだ」と言い張るためには、この壁を乗り越えねばなりません。

## コミットを見分ける方法

`rebase -i`でコミットを思い通りに並び替えるには、まずコミットの見分けを付ける必要があります。これには以下3つの方法があります。

1. コミットメッセージを真面目に書く
2. 変更内容とSHA1の対応をすべて暗記しておく
3. 脇にdiffを開いて見比べながらちまちま並べ替える

怠惰な上記憶力がない人間(筆者)は3番の方法を取らざるを得ません。

コミットは適用する順番を間違えるともとが一本道の歴史でも衝突が起きます[^1]。途中でややこしくなってくると`git rebase --abort`で中断してやり直さざるを得ません。が、`rebase --abort`をするとそこまで頑張って並べ替えたリストを忘れてしまいます。(16パズルをやりなおす時と同じ感覚を覚えます)

## 並べ替える前に通し番号を振っておく

単純なルールとしては、同じファイルを変更するコミットで順序が逆転していなければ、衝突は起きません。並べ替える前に通し番号を振れば、元の順番を思い出せます。

これを自動化しようと思い、冒頭のスクリプトを書きました。通し番号のほかにdiffstatとコミットメッセージもつけましたが、不要なら削ってください。

# 参考リンク

* [Protect our Git Repos, Stop Foxtrots Now! - Atlassian Developers blog](https://developer.atlassian.com/blog/2016/04/stop-foxtrots-now/)
* [`sequence.editor` - git-config(1)][conf]
* [git-rebase(1)](https://git.github.io/htmldocs/git-rebase.html)
* [git-cherry-pick(1)](https://git.github.io/htmldocs/git-cherry-pick.html)
* [How to separate topic branches](https://git.github.io/htmldocs/howto/separating-topic-branches.html)
* [How to revert a faulty merge](https://git.github.io/htmldocs/howto/revert-a-faulty-merge.html)

[conf]: https://git-scm.com/docs/git-config#git-config-sequenceeditor
[^1]: [pijul](https://pijul.org/manual/why_pijul.html)に入門したいのですがまだ勇気が出ません