Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

Git のベースブランチを記録する

この記事は Akatsuki Advent Calendar 2018 の 8 日目の記事です。

このブログでは初めましてになります。id:thinca です。今日は Vim の話…ではなく、お仕事用にごにょごにょしている Git の設定の話をしようかと思います。

背景

私のいるチームでは複数バージョン平行開発が行われており、それもあいまって Git リポジトリには各バージョンや stagingmaster など様々なブランチが存在しています。

そしてそれぞれのブランチから作業用のブランチを作るわけですが、Git では「あるブランチがどのブランチから派生したのか」ということは記録されていません*1。Git の構造を考えると記録されていないのは当然なのですが、これを記録するようにすると、様々な場面で活用できます。そこで私が普段している工夫を紹介したいと思います。

Git の alias 機能

この工夫のために、Git に大量の alias を定義しています。今回の話はほぼ、この alias 集の紹介になります。

各 alias は別の alias に依存しているものもあるので、順を追って紹介していきます。

以後出てくる xxx = yyy の形式の行は、Git の config ファイル内での記述になります。

現在のブランチを取得する

current-branch = symbolic-ref --short HEAD

ありそうでない、現在のブランチを表示するコマンドです。様々なシーンで使うので、最初に紹介しておきます。

ブランチ元のブランチを記録/表示する

base-branch = !git config "branch.$(git current-branch).base"

本稿で伝えたい工夫のコアの部分になります。git config を使って、各ブランチの base と言う名前の項目にブランチ元のブランチ(=base branch)を記録できるようにします。 以下のような感じで使います。

$ git current-branch
new-feature

# ベースブランチを master に設定する
$ git base-branch master

# ベースブランチを表示する
$ git base-branch
master

$

とは言っても、実際にこのコマンドを直接使うことはあまりありません。他の alias から利用する場合が多いです。

ちなみに、こうして記録した内容は .git/config ファイル内に記録されます。ブランチを消したら設定も一緒に消えるので、ゴミが残ったりはしません。

[branch "new-feature"]
    base = master

作業用ブランチを作成する

現在のブランチから作業用ブランチを作成する際、私はよく git cob という alias で行っていました。これは最初は git checkout -b の略だったのですが、base-branch を導入した今では以下のようになっています。

cob = "!f() { \
  b=$(git current-branch) && \
  git checkout -b \"${1}\" && \
  git base-branch \"${b}\"; \
}; f"

alias で引数を受け取りつつアレコレしたい場合のイディオムの 1 つとして、関数を作ってこれを実行しています。他にも方法はありますが、私はこの方法をよく使います。

これで、git cob new-feature を実行すると、new-feature ブランチのベースブランチとして元いたブランチが記録されるようになりました。

分岐地点のコミットを得る

せっかく記録をしても活用しないと意味がありません。そのためにも、追加の補助機能を用意します。

base-commit = !git merge-base origin/$(git base-branch) HEAD
     -o--o  <- new-feature
    /
o--o--o--o  <- master
   ^
   ここを得る

こんな感じで、現在のブランチを切った根本に当たるコミットの sha1 値を得るコマンドになります。これがあると、以下のようなことができます。

現在のブランチ内だけのログを表示

普通にログを表示すると、辿れるだけ際限なく表示されます。今のブランチでは何をやったのかに注目したい場合、その部分だけ表示できると便利です。

fl = !git log $(git base-commit)..HEAD --reverse

ログの表示の仕方は好みがあると思うので、お好みの感じにするとよさそうです。alias の名前も短くて覚えやすいのを付ければよいかと思います。

現在のブランチで変更したファイルを表示

先ほどと同じようなやつですね。これは diff でできます。

bf = !git diff --name-only "$(git base-commit)..HEAD"

変更したファイルだけ、まとめて lint にかけたりする際に便利です。

$ rubocop $(git bf)

特定のバージョンブランチから派生した作業用ブランチ一覧を表示する

あちこちのバージョンで気まぐれに始めては途中で止まっているブランチがあるため、通常の git branch によるブランチ一覧だとどのブランチがどのバージョン上で作業していたものなのかがわかりづらくなってしまいます。そこで、今いる(あるいは指定した)ブランチから作られたブランチの一覧を表示できるようにします。

features = "!f() { \
  git branch | \
    cut -c3- | \
    while read b; \
      do echo \"$(git config \"branch.${b}.base\") -> ${b}\"; \
    done | \
    grep -v '^ -> ' | \
    grep \"^${1:-$(git current-branch)} ->\" | \
    sort; \
}; f"

こういう感じで結果が見れます。

$ git features master
master -> feature-a
master -> feature-b

$

rebase する

当然 rebase も簡単にできます。

rbu = !git rebase "origin/$(git base-branch)"
     -o--o  <- new-feature
    /
o--o--o--o  <- master

 |
 v

           o--o  <- new-feature
          /
o--o--o--o  <- master

fetch によって origin が伸びている場合が多いので、origin のブランチの先頭に対して rebase するようにしています。 実は git base-branch でもこっそり origin を見ていました。これは今回のように rebase することがあるためです。

base を移動する

version-a ブランチからブランチを作ってやってる作業を version-b に入れることになった!となったら、rebase を利用すればサクッと移動できます。

move-base = "!f() { \
  [[ -n $1 ]] && \
  git rebase --onto $1 $(git base-branch) && \
  git base-branch $1; \
}; f"

git move-base version-bversion-b に移動できます。

現在の差分をブランチ中の特定のコミットに混ぜ込む

ブランチで作業中に、途中で typo を入れ込んだことに気付いた時、新たに Fix typo なんてコミットを積む人はいないと思います。後から見て履歴がわかりやすくなるように、最初から typo を入れないように履歴を修正するでしょう。となったら rebase による履歴改変の出番となるわけですが、普通にやると意外に面倒です。これもサクッとできるようにしています。

まずは混ぜ込む先のコミットを簡単に選べるようにしたい、ということで以下は fzf を使ってコミットを選択するコマンド。コミットの範囲を引数に渡します。previewgit show を使ってコミットの中身を見つつ選択できるようにしてあります。

select-commit = "!f() { \
  git log --oneline ${1} | \
    fzf --preview='git show --color {1}' | \
    cut -b 1-7; \
}; f"

続いて現在の差分を、この git select-commit を使って選択したコミットに対する fixup コミット*2としてコミットするコマンド(わかりづらい)が以下。

fixcommit = "!f() { \
  git commit --fixup=\"$(\
    git select-commit \"${1:-$(git base-commit)}..HEAD\"\
  )\"; \
}; f"

git base-commit を使って適切な範囲のみを候補として表示するようにしています。

さて、fixup コミットを作ったので、これを適用します。適用するためのコマンドが以下。

fixup = !git -c core.editor=true rebase -i --autosquash $(git base-commit)

base を記録しているおかげで、rebase を機械的に実行できます。--autosquash を付ければ最初から fixup 済みの状態でエディタが開くので、編集する必要もありません。編集しなくてもよいので、エディタとして true コマンドを指定して、すぐに閉じています。これで fixup を適用できます。

ここまで来たらまとめてやってしまいたいので、まとめてやるコマンドを用意しています。

fix = "!f() { \
  git fixcommit ${1} && \
  git fixup; \
}; f"

つまり、typo 直したい!となったら、

  1. 直して git add
  2. git fix
  3. fzf が起動するので対象コミットを選択
  4. 混ぜ込み完了

となります。簡単ですね。

GitHub の PR 作成ページを開く

近ごろの GitHub はとても便利なので、新規ブランチを push すると「このページにアクセスすれば PR 作れるよ!」と教えてくれます。*3

remote: Create a pull request for 'new-feature' on GitHub by visiting:
remote:      https://github.com/aktsk/awesome-product/pull/new/new-feature

しかし、いざこのページにアクセスすると base はデフォルトブランチとしてページが開きます。デフォルトブランチと開発ブランチでは差が大きくなっていることは珍しくなく、差分の生成のために GitHub を開くのが重くなります。こんなページは開きたくない。

そこで、直接!目的の base に対して PR が作れるページをブラウザで開くコマンドです。ブラウザを開くために open を使っているので、MacOS 専用です。

gh-repo = "!git remote get-url origin | sed 's/.*:\\(.*\\)\\.git/\\1/'"
mkpr = "!open https://github.com/$(git gh-repo)/compare/$(git base-branch)...$(git current-branch)?expand=1"

まとめ

今回は、Git にベースブランチを記録することで様々な作業を効率化する例を紹介しました。Git の alias の設定は config ファイル 1 つで管理できるため、環境間での取り回しがやりやすいです。その分複雑なことをしようとするとメンテナンスが難しくなってしまいますが、その点は適切に改行を入れたり、コマンドを分割することで緩和してみました。

小さな工夫で作業を効率化できることは多いので、不便に感じたら積極的に効率化して行きましょう。

*1:単純なリポジトリであればある程度推察できる場合がありますが、複雑になると失敗するので確実性がありません。

*2:fixup コミットについてはこちらの記事等が参考になります。

*3:この URL は例なので実在しません。