gin.vimで捗るgitのログ改竄 (instant fixup)

by
カテゴリ:

Vim 駅伝の2024/3/15の記事です。

Gitで整然とコミットを詰むのはそうそうたやすいものではありません。
あのコミットでバグを仕込んでしまった、コミットメッセージを間違えていた、そんなミスはよくあることです。
かと言って、整然とコミットするためにコミットを後回しにしては本末転倒です。
うかつな操作で作業内容を失うかもしれませんし、少し前の作業内容に戻りたくなるかもしれません。
また差分が大きくなるほど適切な粒度でのコミットが億劫になります。

そこで、VimでGitを操作するためのgin.vimというプラグインを使い、コミットの修正を簡単にするinstant fixupを実装しました。
magitのinstant fixupやlazygitのAmend an old commitに類する機能です。

デモ

以下の動画のように、特定のコミットに後から差分を追加したり、メッセージを変更したりできます。
便利なので使ってみてください。

instant fixupを使えると、とりあえずコミットを積んで後から改竄が容易です。
改竄しやすいコミットの積み方は意識した方がいいかもしれません。
本質的にはgit rebaseをしているのですが、大規模なrebaseはコンフリクトの元です。
instantにできることはさっさとやっておくと、コミットの並べ替えや合体といった複雑な操作を後から集注してできるのも魅力ですね。

解説

具体的な手順は以下のような感じ

  1. 修正したいコミットに混ぜたい変更をステージングしておく
  1. :GinLogを使ってログを表示する
  1. 修正したいコミットにカーソルを合わせてa<Plug>(gin-action-choice))を実行する
  2. 修正方法に合わせて必要なinstant fixupを選択

生のgitコマンドでやろうとすると、以下のようにそこそこ煩雑な操作です。
まずはgit commit --fixupgit rebase --interactiveを覚えるべし……という気持ちもありつつ、生ではinstant感を得られませんね。

  1. git addで修正対象のコミットに含める差分をstagingしておく
  2. git logで修正対象のコミットを特定する
  3. git commit --fixup {commit}で指定したコミットを修正するためのコミットを積む
  4. git -c sequence.editor=true rebase --interactive --autostash --autosquash --quiet {commit}~で修正対象のコミットに修正内容のコミットを混ぜつつ、以降のコミットを積み直す

Tips

マッピングでやりたい

Ginのアクションは実際には<Plug>(gin-action-fixup:instant-fixup)などといったマッピングで定義されています。
頻繁に使うアクションは以下のようにバッファローカルなマッピングを定義すると便利かもしれません。

nnoremap <buffer> if <Plug>(gin-action-fixup:instant-fixup)
nnoremap <buffer> ir <Plug>(gin-action-fixup:instant-reword)

:GinLogで表示しているコミットログはset nomodifiableされているため、編集できません。
従ってiでインサートモードに入ることもないので、ifirをinstant fixupやinstant rewordの略称として用いるのはそれなりに合理的だと思います。

Fuzzy Finderでやりたい

Ginのアクションがマッピングであることは先にも述べた通りです。従ってキーマップをfuzzy findして実行するようなプラグインを利用すると、目的のactionを探しやすくなります。
たとえば以下のようにすると、aで本来はGinが実装しているアクション選択モードに入るところを、Telescopeによるアクション選択に差し替えられます。

vim.api.nvim_create_autocmd("BufReadCmd", {
  group = vim.api.nvim_create_augroup("gin-custom", {}),
  pattern = "ginlog://*" ,
  callback = function(ctx)
    vim.keymap.set("n", "a", function()
      require("telescope.builtin").keymaps({ default_text = "gin-action " })
    end, { buffer = ctx.buf })
  end,
})

ENJOY!!!