Vim
Git
vimscript
vim-plugins
git-rebase
VimDay 13

git rebase -iの時に役立つプラグイン

はじめに

以前、と言っても結構前ですが、タイトルにあるようなgit rebase -iの時に役に立つVimプラグインというのを作ったので、それを紹介したいと思います。

動機

僕の所属している開発チームでは、バージョン管理システムにgitを使用しています。

gitは広く知られている通り、分散バージョン管理システムと呼ばれているものの一つです。その特徴と言っていいのかわからないですが、gitを利用すると、手元でのソースコードの変更を、細かい単位でローカルのリポジトリにどんどんコミットしておき、それを適当なタイミングでコミット履歴を改変して内容を整理してから、チームで共有しているリポジトリに状態を同期させるようなことができます。

git rebaseとは、そのようにコミット履歴を改変するときに使用するコマンドです。

git rebaseコマンド、特に-iというオプションを付けたものは、コミットの履歴を入れ替えたり、複数のコミットを1つにまとめたり、コミット内容を再修正する目的で特定のコミットを適用する直前の状態を復元したりできる、非常に強力で便利なものです。

詳しい利点や使い方は他に詳しいサイトがあるので、そちらをご参照ください。

さて、僕はこのgit rebase -iというコマンドをよく利用しているわけですが、このコマンドによって、コミット履歴をどのように改変するかを指定するときに vim の出番となります。

シェルでgit rebase -iコマンドを実行すると、コミットIDとコミットメッセージが列挙されたgit-rebase-todoというテキストファイルが一時的に作成され、そのファイルが、環境変数EDITORで指定されたテキストエディタ(vimなど)で開かれます。

このファイルの内容を変更して保存し、テキストエディタを終了すると、変更した内容に従ってコミットの履歴が改変されます。

git-rebase-todoファイルの中身は、下のようになっています。

git-rebase-todo
pick 60816c6 Make some overriding functions private.
pick 81aa91d Adjust sub-components position depending the component size.
pick 539d099 Add binary images as source files.
pick becba38 Add image resource file to the jucer file.
pick 410cd2e Add juce_audio_utils module
pick cefdb2f Add BinaryResource.h & cpp to the jucer file.
pick 13a4ca9 Add missing jucer setting.
pick 31407b3 Add SoundTouch settings.
pick ac15e5c Reset include header path for SoundTouch.
pick 8925057 Don't show frame of components.

# Rebase 1f7bb2c..8925057 onto 1f7bb2c (10 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

上の規則的に並んだ行がコミット履歴を表しており、git rebase -iコマンドを実行する時に指定した範囲のコミットが、古い順に上からリストアップされています。

各行のカラムは左から、

  • 各コミットをどのように改変するかを指定するコマンド
  • コミットID
  • コミットメッセージ

となっています。

単にこれらの行の順番を入れ替えると、あたかもコミットしたのがその順番であったかのように、コミット履歴を入れ替えられます。

あるいは、左端のカラムにかかれているコマンドを変更すると、あるコミットを一つ上のコミットにまとめたり、コミットメッセージを変更したりできます。

さて、ではこのファイルを編集して履歴を改変しよう、ということになるのですが、困ったことに、実際にはこれだけの情報からコミット履歴を改変するのはなかなか辛いものがあります。というのも、各コミットで行った変更内容が、ソースレベルでどのようなものだったかは、詳しく覚えていられません。

もし2つのコミットの内容に依存関係があった場合、それらコミットの順番を不用意に変更してしまうと、うまくrebaseができなくなります。rebaseのタイミングでそれを確認できないのは不便です。

あるいは、手元では細かく複数のコミットを作っていたけれども、共有リポジトリに同期する前にコミットをもっと大きい単位でまとめたいという状況もあるかもしれません。その時も、どのコミット同士をまとめるときれいになるかは、ソースコードを参照しながら決めたくなります。

作ったもの

この問題を解決するために、Vimプラグインを作成しました。

https://github.com/hotwatermorning/auto-git-diff

このプラグインを使用すると、git rebase -iコマンドによるgit-rebase-todoファイルの編集のときに、各コミットの内容を、分割したウィンドウにプレビュー表示できます。

ezgif.com-optimize (1).gif

このプラグインは、dein.vimのようなパッケージマネージャを利用してインストールだけすれば、特に設定を行わなくても使用できます。

実は、このプラグインを使わなくても、git-rebase-todoファイルを開いたときに<Shift-k>を入力すればそのコミットが参照できます。しかしその方法でコミット内容を表示すると、今度はgit-rebase-todoファイルが隠されてしまい、コミット内容を見ながら他のコミットとの関係を考えることができません。

このプラグインを使えば、対象となるコミット履歴とコミット内容を同時に参照しながら、どのような改変を行うかをインタラクティブに精査できます。

解説

小さなプラグインなので、特に解説することもないのですが、(さらに、僕自身忘れている所も多いのですが)一応書いておきます。

このプラグインは、git-rebase-todoファイル内でのカーソルの移動を監視します。

そして、カーソルの移動によって、カーソルのある行のコミットIDが変更されたら、プレビューウィンドウを新しいコミットIDの内容に更新します。1

プレビューウィンドウの内容は、カーソルの現在行のコミットIDと、そのコミットの直前のコミット(画面上で隣り合うものではなく、実際の履歴で直前のコミット)とのdiffになっています。

デフォルトでは、git diff --stat -p --submodule -C -C <対象のコミット>~1 <対象のコミット>というオプション付きでgit diffコマンドを呼び出した結果が使用されますが、このコマンドは、g:auto_git_diff_command_optionsという変数によってカスタマイズ可能です。特に、-C -Cというオプションは負荷が高いとgitのヘルプに記載されているので、大きなプロジェクトで変更内容が多いときには、これを-Cだけにしたほうがいいかもしれません。2

他にも変数を用意していますが、個人的にはほとんどカスタマイズしていないので、バグとかがあるかもしれません。気づいた方はお知らせいただけると大変ありがたいです。

最後に

このプラグインは、個人的に非常に重宝しています。

もしgit rebase -iを多用していて、その時のエディタにvimを使用している人は、ぜひこのプラグインを使ってみてください。

きっと、その作業が1000倍楽になりますよ。3



  1. 最初の版では、カーソルの移動の度にコミット内容を再取得して画面を更新していました。それはあまりにむだなので、今はカーソル移動によって現在行のコミットIDが変化したときにのみ表示を更新するという仕組みになっています。この対策は、 chiastoliteさんに協力いただきました(#4)。多謝。 

  2. https://git-scm.com/docs/git-diff 

  3. https://twitter.com/ssoriche/status/800828330375344128