😇

Git の効率を爆上げするスクリプト集

2021/06/20に公開

ここ数日間開発効率そこあげするためにツール周りを見直している毎日です。

peco はすごいですね。めちゃくちゃ便利です。こんなに可愛らしい名前なのに。もりもり食べて吐き出してくれます。

と言うわけで peco を使った、何番煎じか分かりませんが、よく使う Git 操作を効率化するスクリプトです。
モバイルアプリエンジニアでも最低限この辺はよく使うので、シュッと使って生産効率爆上げしていきましょう!

macOS + zsh でしか動作は確認していません...
なんかめっちゃインデントずれてる....見にくくてすみません.....

git diff

git diff の利用頻度が圧倒的に多い自分なのでまずは。

基本的には status を ls した結果を peco に食べさせて diff を表示する感じです。

入力するコマンドは少なければ少ないほど時間短縮になる、それが利用頻度が高ければ高いほど、と思います。
なので先頭1文字で動くように alias をはります。

ここで思ったのは、peco で選ぶのが手っ取り早いけどパス指定して取る事もあるよな、と言うことです。
でもそのために diff_pecodiff_path みたいなのを複数作って alias 両方はるのはファイル内の可読性も悪くなるし覚える事増えて効率悪くなるじゃん...

と、言うことで、オプション的な感じで切り替えれたらいいのではと思い getopts を使う事にしました。

-f <path> でパスのファイルの、 -s で peco の出力の diff が出力されます。

-s した後は add なり checkout なりしたいと思うのでクリップボードにパスをコピーします。

alias d='_git_diff'
function _git_diff() {
	local selected_file="."
	while getopts ":sf:" opt; do
		case $opt in
		  f) selected_file="$OPTARG"; break ;;
		  s)
		  	selected_file="$(git ls-files -m | peco)"
		  	if [ -z $selected_file ]; then
		  		echo "No selected for diff."
		  		return 1
		  	fi
		  	echo "$selected_file" | pbcopy
		  	break
		  	;;
		  ?)
			echo "Unknown option selected."
			return 1
			;;
		esac
	done
	git diff $selected_file
}

以降、基本的に peco と getopts を使ったこんな感じのスクリプトが続きます。

git add

add だけ -p をよく使うのでファイル選択に加えてそれもコマンドに反映させる様にしています。
それと 勝手に全ファイル add されると困るので -a を付けた時だけにしています。

alias a='_git_add'
function _git_add() {
	local selected_file=""
	local options=()

	while getopts ":spaf:" opt; do
		case $opt in
		  f) selected_file="$OPTARG" ; break ;;
		  a) selected_file="." ; break ;;
		  s)
		  	selected_file="$(git status --porcelain -s | peco)"
		  	if [ -z $selected_file ]; then
		  		echo "No selected for add."
		  		return 1
		  	fi
		  	break
		  	;;
		  p) options+=("-p") ;;
		  ?)
		       echo "Unknown option selected."
			   return 1
			   ;;
		esac
	done
	git add $options[@] ${selected_file##* }
}

git restore

add した差分をやっぱり元に戻したい時に使います。

alias r='_git_restore'
function _git_restore() {
	local selected_file=""
	while getopts ":saf:" opt; do
		case $opt in
		  f) selected_file="$OPTARG"; break ;;
		  a) selected_file="."; break ;;
		  s)
		  	selected_file="$(git diff --name-only --diff-filter=M --staged | peco)"
		  	if [ -z $selected_file ]; then
		  		echo "No selected for restore."
		  		return 1
		  	fi
		  	break
		  	;;
		  ?)
				echo "Unknown option selected."
				return 1
				;;
		esac
	done
	git restore --staged $selected_file
}

git checkout file

diff 削除はかなり危険なのでほんとにやるか最終チェックを入れています。
ブランチ操作の checkout とは棲み分けたかったため名前を変えています。

alias dd='_git_discard_diff'
function _git_discard_diff() {
	local selected_file=""

	while getopts ":sf:" opt; do
		case $opt in
		  f) selected_file="$OPTARG"; break ;;
		  s) selected_file="$(git ls-files -m | peco)"; break ;;
		  ?)
				echo "Unknown option selected."
				return 1
				;;
		esac
	done

	if [ -z $selected_file ]; then
		echo "No selected for restore."
		return 1
	fi

	echo -n "Really discard $selected_file changes? "; read answer
		case $answer in
	  	[yY] | [yY]es | YES )
	  		git checkout $selected_file
	    	;;
	  	* )
	  		echo "No actions."
	  		return 1;;
	  esac
}

git checkout

-l でローカルブランチのリストを、-r でリモートブランチにリストをそれぞれ peco に食べさせています。

-s <branch> で指定したブランチにチェックアウトします。

-h で head からのコミットハッシュのヒストリーを peco に食べさせて、選んだコミットにチェックアウトします。

-b <branch name>git checkout -b をします。

alias ch='_git_checkout'
function _git_checkout() {
	local selected_branch=""
	local options=()

	while getopts ":lrhsb:s:" opt; do
		case $opt in
		  s) selected_branch=$OPTARG; break ;;
		  h) selected_branch="$(git log --oneline | peco | cut -d ' ' -f 1)"; break ;;
		  l) selected_branch="$(git branch | peco)"; break ;;
		  r) selected_branch="$(git branch -r --sort=-authordate | perl -pe 's#origin/###' | peco)"; break ;;
		  b)
		  	selected_branch=$OPTARG
		  	options+=("-b")
		  	break
		  	;;
		  ?)
				echo "Unknown option selected."
				return 1
				;;
		esac
	done

	if [ -z $selected_branch ]; then
		echo "No branch selected."
		return 1
	fi

	git checkout $options[@] ${selected_branch##* }
}

git branch

git branch する時は、だいたいそのブランチ名をコピペして merge したり rebase したりしたいと思います。

なので今回は branch のリストアップは peco がやってくれるので標準出力はせず、クリップボードにコピーします。

alias b='_git_branch'
function _git_branch() {
	local selected_branch=""
	while getopts ":lr" opt; do
		case $opt in
		  l) selected_branch="$(git branch | peco)"; break ;;
		  r) selected_branch="$(git branch -r --sort=-authordate | perl -pe 's#origin/###' | peco)"; break ;;
		  ?)
				echo "Unknown option selected.";
				return 1
				;;
		esac
	done

	if [ -z $selected_branch ]; then
		echo "No branch selected."
		return 1
	fi

	echo ${selected_branch##* } | pbcopy
}

git push

current branch を origin にプッシュします。

alias po='_git_push_current_branch'
function _git_push_current_branch() {
  local checkouted="$(g b --show-current)"
  echo -n "Really push to origin/${checkouted}? "; read answer
  case $answer in
  	[yY] | [yY]es | YES )
  	  git push origin $(g b --show-current);;
  	* )
  	  echo "No actions."
  	  return 1;;
  esac
}

git cherry-pick

cherry-pick したい時はだいたい別のブランチのログからハッシュ値をコピペしてくるんですが、その辺もっと上手くできたらいいですよね。

alias chp='_git_cherry_pick'
function _git_cherry_pick() {
	while getopts ":s:r:" opt; do
		case $opt in
			r)
		  	local start="$(git log --oneline $OPTARG | peco | cut -d ' ' -f 1)"
		   	local end="$(git log --oneline $OPTARG | peco | cut -d ' ' -f 1)"
				git cherry-pick $start..$end
				;;				
		    s)
		  	local commit="$(git log --oneline $OPTARG | peco | cut -d ' ' -f 1)"
				git cherry-pick $commit
				;;
			?)
				echo "Unknown option selected."
				return 1
				;;
		esac
	done
}

git bisect

ここからここまでの間でなんか悪さしてるんだよな〜〜、って事がよくあるので毎回 tig からコミットハッシュ調べてコピペして bisect してたんですが、それもその時選べると効率 UP ですよね。

-s でコミットハッシュのヒストリーから始点と終点を選んで start します。

alias bs='_git_bisect'
function _git_bisect() {
	while getopts ":sbgr" opt; do
		case $opt in
			b) git bisect bad ;;
			g) git bisect good ;;
			r) git bisect reset ;;
			s)
				local bad="$(git log --oneline | peco | cut -d ' ' -f 1)"
				local good="$(git log --oneline | peco | cut -d ' ' -f 1)"
				git bisect start $bad $good
				;;
			?)
				echo "Unknown option selected."
				return 1
				;;
		esac
	done
}

git rebase -i

あ、この差分3つ前のコミットに含めるやつだった :innocent:

みたいな事がままあります()

そういう時もコミットハッシュ引っ張ってこれたら楽ですよね。

alias rbi='_git_rebase_interactive'
function _git_rebase_interactive() {
	local start="$(git log --oneline $OPTARG | peco | cut -d ' ' -f 1)"
	git rebase -i $start
}

develop ブランチを更新

git flow に則っているところでは develop ブランチを運用しているのではないでしょうか。
develop は日々更新されていくのでローカルでも常に最新の状態にしておきたいですね。

なので develop の fetch & rebase をします。
最後は元いたブランチに戻ります。

本来はちゃんと fetch してアップデートの内容確認して rebase するのが正しいやり方なので、完全に更新内容が分かってる時に使いましょう。

個人的に git pull は嫌いです...

function update_develop() {
	local was_there="$(git branch --show-current)"
	git fetch && git checkout develop && git rebase origin/develop
	git checkout $was_there
}

番外編

git alias

ご存知の方もたくさんいると思いますが、 git も alias がつけれるので init や status など peco らなくても問題なさそうなものはそちらに登録しておくとさらに効率あがります。

[alias]
	s = status
	r = reset
	c = commit -m
	md = merge develop

みたいにしておくと git s で status が確認できますし、git md で develop ブランチをマージできます。

さらに zsh の alias に alias g='git' とかしておくと g s ですみます!

これを alias s='g s' とかしておくともう1文字で status が確認できます!

もはやなんなんだ...

tig からコミットハッシュをコピー

↑ でやってきたコミットハッシュの取得はコミットメッセージしか表示されないので、差分を見ながら「あぁ、これこれ」って感じでハッシュを取ってきたい場合は tig を見ながらやれると嬉しいですね。

bind generic y @sh -c "echo %(commit) | pbcopy"

こうすると、tig を開いてる時に y を入力するとカーソルが当たってるコミットのハッシュ値がクリップボードにコピペされます。

これを git cherry-pick とかすると幸せになれるかも。

最後に

タイトルのわりにはいろんな方の dotfiles に書いてある様なスクリプトでした!!
自分の dotfiles にも載せています。

https://github.com/tick-taku/dotfiles/blob/master/component/zsh/.zshrc.mine.funcs

peco、いいですね。
僕が愛して止まない NEW GAME! と言うおじさんの擬人化漫画にも同じ名前が出てくるので愛着があります。

オプション多用してるので、はたして効率が良くなっているのかは意見が分かれそうですが、慣れれば爆速で Git 操作ができる様になるので機能実装に注力できる様になります。

参考

https://qiita.com/Esfahan/items/e88bb806c7ca1dc8b758

https://qiita.com/vivid_muimui/items/7e7a740e6537749de0c0#bind

Discussion