【Git】エイリアス晒す＆書き方まとめ

[Git Advent Calendar 2019](https://qiita.com/advent-calendar/2019/git) 1日目です。
2日目は@100さんの[【VRChat】継続的なクロスプラットフォーム対応](https://gitlab.com/vrc-gitcommitter/wiki/wikis/%E3%80%90VRChat%E3%80%91%E7%B6%99%E7%B6%9A%E7%9A%84%E3%81%AA%E3%82%AF%E3%83%AD%E3%82%B9%E3%83%97%E3%83%A9%E3%83%83%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E5%AF%BE%E5%BF%9C)です。
# TL;DR
```bash:.gitconfig
[alias]
    #以下の3種は全て同じコマンドが実行される

	# 単純な置き換え
	st = status -uno
	
	# 先頭に!を付けると、その後のコマンドを外部コマンドとして("git "を付けずに)シェルスクリプトで実行
    stex = !git status -uno

	# 関数を使うと引数を任意の場所に展開できる
	stfunc = !"f(){ git stauts -uno \"${@}\"; };f"
```

#はじめに
Gitも気付いたら`2.24.0`になっていて(2019/12/01現在)、`git switch`とか新しいコマンドが生えててどんどん使いやすくなっている訳ですが、それでも我々は記憶量とタイピング量を減らしたい。人類の脳みそはそんなに強くない。
という訳でGitというよりシェルスクリプトの初級編、エイリアスについて。CUIをちょっと楽にしような。

## Gitのエイリアスとは
`Git`のコマンドの「別名」を付ける機能のこと。
タイピング量を減らしたり、覚えやすいコマンド名に置き換えるために利用される。`git エイリアス名`と打つと設定したコマンドが実行される。つまりシェルに任意のコマンドを一行で展開する機能として使えるので、複数のコマンドを繋げたり、それ単体で簡単なシェルスクリプトを組む芸当も可能。

## 設定ファイルの場所
エイリアスの設定を行うには`git config --global alias.エイリアス名 エイリアス先のコマンド`を打つ、などと[エイリアスの公式ドキュメントには書かれている](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases)。が、後述する特殊文字のエスケープに加えて「`git config`コマンドを解釈するときのエスケープ」が必要になるため非常に面倒くさい。それよりは`config`ファイルを弄ってしまった方が楽だと思う。

`config`ファイルの場所は参考URLを参照のこと。
参考：[gitconfig の基本を理解する](https://qiita.com/shionit/items/fb4a1a30538f8d335b35)

以下ではユーザー単位の`~/.gitconfig`に記述するものとする。
`.gitconfig`などの設定ファイルに`[alias]`と書くことで、それ以降～次の`[---]`までがエイリアスの設定として認識される。
基本的には1行 = 1コマンドである。複数行に記述したいときは行末に`\`を置く。

#書き方まとめ
## 単純な置き換え
```bash:.gitconfig
[alias]
st = status -uno
```
最も単純な方法。エイリアスに登録した文字列が置き換わる。
この場合、`git st`と打つと`git status -uno`が実行される。従って、Gitのコマンド以外は指定できない。
オプションを付与したい場合は後ろに追記する。この場合、例えば`git st -s`と打てば`git status -uno -s`が実行される。

## 外部コマンド
```bash:.gitconfig
[alias]
stex = !git status -uno
```

エイリアスの冒頭に`!`を付与することで、`git ~`が丸ごと置き換えられる。この置き換え先はシェルコマンドとして解釈される。従ってGit以外のコマンドを利用することが可能。
ちなみにこの方法で「エイリアスのエイリアス」を無限に作成できる。

```bash:.gitconfig
[alias]
#特に意味はない
super_status = !git st
extra_status = !git super_status 
ultra_status = !git extra_status
```
また、適切な制御記号を入れることで複数のコマンドを繋げることが可能。

```bash:.gitconfig
[alias]
#git fetchが完了したらログを直近20個分出して、最後にstatusを確認
now = !git fetch; git graph -20 --date-order; git st
```

以下はエイリアスというよりシェルスクリプトの勉強内容だが、コマンド間の制御記号と効果についてはこんな感じ。

| 記号       |      効果 |
|:----------:|:------------------|
| ;| 順番に実行|
| && | 前のコマンドが成功したら実行|
| &#124;&#124; |前のコマンドが失敗したら実行|
| &#124; |前のコマンドの標準出力を次のコマンドの標準入力にする|
|&|前のコマンドをバックグラウンドで実行(つまり待たない)|

一番最後の`&`はコマンドの途中に入れるとエイリアス(の実行シェル)が終了しても終了しないので、基本的には外部のプログラム(エディタなど)を起動したい時などに利用する。

## 関数にする
```sh:.gitconfig
[alias]
stfunc = !"f(){ git stauts -uno \"${@}\"; };f"
```
シェルスクリプトは`f(){ コマンド }`で関数を定義し、`f`でその関数を実行できる。従って`"f(){ コマンド };f"`で関数を定義し、即座に実行できる。両端の`""`が無いとsyntax errorとなる。
このとき、変数`${1}`~`${n}`に個別の引数が、`${@}`に全ての引数が、`${#}`に引数の数が、それ以外にもいろいろ入る。
参考：[初心者向けシェルスクリプトの基本コマンドの紹介 #特別な変数](https://qiita.com/zayarwinttun/items/0dae4cb66d8f4bd2a337#%E7%89%B9%E5%88%A5%E3%81%AA%E5%A4%89%E6%95%B0)

見やすさのために改行したい場合は以下の様に、行末に`\`を書く。これはシェルに渡される時点で一行に戻されて実行される。

```sh:.gitconfig
[alias]
stfunc = !"f(){ \
    git stauts -uno \"${@}\";\
 };f"
```

なお引数を使うための方法として`!sh -c \"git status\"`のような記述方法もあるが、「プロセスが多重起動すること」「Gitのエスケープとshのエスケープが両方必要になって記述が面倒くさくなること」から[投げ捨てた方が良い](https://stackoverflow.com/questions/46695859/should-i-use-sh-c-or-f-f-in-git-alias-scripts)、とのこと。

参考：[関数の使用方法 UNIX & Linux コマンドおよびシェルスクリプトリファレンス](https://shellscript.sunone.me/function.html)
参考：[位置パラメーターの一括展開 $* $@ "$*" "$@" の違いを知れ!!](https://fumiyas.github.io/2016/12/15/positional-parameters.sh-advent-calendar.html)


#登録時の注意点
### 大文字/小文字について
`git status`などの**本来の**`Git`のコマンドは大文字/小文字を区別しなければならない。しかし、エイリアスで`st = status`と設定した場合は大文字/小文字を無視する。つまり`git ST`でも`git St`でも`git sT`でも全て`git st`と同じ動作をする。逆に言うと大文字/小文字で別のエイリアスを登録することはできない。
ついでにGitのコマンドと同名かつ大文字/小文字違い、つまり`STATUS = status`などとしても、`fatal: cannot handle STATUS.exe as a builtin`として怒られる。名前探索の最初の段階で`git-~`と一致するか否かを見ている模様。

### 外部コマンドが動作するディレクトリ
単純な置き換えのときはカレントディレクトリから移動することは無い。しかし、`!`によって外部コマンド呼び出しにすると**そのときのgitが動いている(`.git`フォルダがある)ディレクトリ**に移動してから動作する。

```bash:.gitconfig
[alias]
pwd = !pwd
```
試しにこういうエイリアスを作って`pwd`と`git pwd`を見比べてみる(Gitのリポジトリ位置は`/c/source/alias_test/RemoteRepo`とする)。

```bash:Terminal
usern@me MINGW64 /c/source/alias_test/RemoteRepo (master)
$ cd Child/

usern@me MINGW64 /c/source/alias_test/RemoteRepo/Child (master)
$ pwd
/c/source/alias_test/RemoteRepo/Child

usern@me MINGW64 /c/source/alias_test/RemoteRepo/Child (master)
$ git pwd
/c/source/alias_test/RemoteRepo
```
カレントディレクトリを`/Child`に移動してから`pwd`を記述すると当然`(Gitのレポジトリ)/Child`が表示されるが、`git pwd`と打つと`(Gitのリポジトリ)`に移動していることが分かる。これは[公式ドキュメント](https://git-scm.com/docs/git-config#git-config-alias)に書かれている。
> Note that shell commands will be executed from the top-level directory of a repository, which may not necessarily be the current directory. GIT_PREFIX is set as returned by running git rev-parse --show-prefix from the original current directory.
> 注：それらの(`!`を付与された)シェルコマンドはリポジトリのトップレベルディレクトリで実行されます。これはカレントディレクトリと一致するとは限りません。このとき、カレントディレクトリで`git rev-parse --show-prefix`を実行した結果が`GIT_PREFIX`にセットされます。

このため、エイリアスでGit内のファイルを参照したい場合などはリポジトリのディレクトリからの相対位置だけ考えれば良い。
一方で`git ls-files`のようなカレントディレクトリの位置によって結果が変わるコマンドを使いたい場合、この挙動は困るかもしれない。公式ドキュメントによれば変数`GIT_PREFIX`に相対パスの値が入るよ、とのことなので、下記のように`cd ${GIT_PREFIX}`を先に書いてあげれば良いようだ。

```bash:.gitconfig
[alias]
# git ls-filesを行うだけのエイリアス。これ自体に意味は無い
ls-files-alias = !cd ${GIT_PREFIX} && git ls-files;
```

参考：[git aliasの外部コマンド呼び出しはgit管理下のルートディレクトリから呼ばれる](https://ark4rk.hatenablog.com/entry/2019/01/30/015458)


### エスケープの仕様(未完成)
特に関数を使う場合、いくつかの特殊文字のエスケープが必要となる。関数を`"f(){ コマンド };f"`で記載する都合上、`""`に囲まれた範囲のどの文字をシェルに展開すればよいのかを明示しなければならない。
確認できる範囲では、`"\`は少なくとも`\`でエスケープする必要がある。例外として、`if`直後の`[...]`ブロック中は(これ自体が単体のコマンドなので)エスケープしなくても動く……たぶん。
どの文字がどの状況でエスケープ対象になるのか、ご存知の方がいらっしゃれば編集リクエスト頂きたく。

#晒す
手元に設定しているエイリアス一覧はこんな感じ。
下の方にあるやたら長いエイリアスは以下の記事参照。
[特定時点の特定ファイルをエディタで開くgit-alias](https://qiita.com/YamEiR/items/7002ec1869717b7da9ce)
[【git】checkoutせずに特定ブランチをpullするエイリアス](https://qiita.com/YamEiR/items/630fcdd3c4c1984fdc2a)

```bash:.gitconfig
[alias]
	# git alias : 現在登録してあるエイリアス一覧を表示する. 
    # 長いエイリアスが多いので前方42文字で切っている
	alias = !git config --get-regexp '^alias\\.' | sed 's/alias\\.\\([^ ]*\\) \\(.*\\)/\\1\\\t => \\2/' | cut -c 1-42 | sort

    # git st : トラッキングしているファイルのステータスのみ表示. 
	st = status -uno

    # git graph : logを良い感じにグラフで表示.
    # 全てのブランチを表示したいので --remotes と --branches を追加している
    graph = log --graph --date=format:'%m/%d %H:%M' --format=format:'%C(yellow)%h %C(cyan)%ad %C(green)%an%Creset%x09%s %C(red)%d%Creset' --remotes --branches

	# git now : 現在欲しい情報(直近20個のログとステージング)を更新してから表示. 
    # 便利だが、同名のフリーコマンドがあるらしいので別名にした方が良さそう.
	now = !git fetch; git graph -20 --date-order; git st

    # git F5 : git now が他のコマンドと被っているので別名を模索していた最中の産物
    # なんとなく「F5キーって更新っぽいよな」というだけ。
	F5 = !git now

    # git cm : commitの省略形。そんだけ。
	cm = commit
	cmm = commit -m
	cmam = commit -a -m

    # mergetool系
    # 大本の設定とかはこちらを参考にした → https://qiita.com/kobake@github/items/fb317b4fdacad718a4b2
    # difftoolをそのまま起動するとシェルが待機状態になってしまうので、関数形式にして末尾に`&`を付与し並列実行させている.
    windiff = "!f(){ git difftool -y -d -t WinMerge /"${@}/" & };f"
    winmerge = mergetool -y -t WinMerge
    # git winshow <コミットハッシュ等> : 一つ前のコミットと比較してWinMergeで表示する.
    winshow = "!f(){ git difftool -y -d -t WinMerge ${1}~..${1} & };f"
	
	# git-flow系
    # ぶっちゃけコマンドを毎回忘れるから設定しておくだけ
	fi = flow init -d
	ff = flow feature
	fr = flow release
	fh = flow hotfix

	#情報取得系
	# git where <コミットハッシュ>  そのコミットが属しているブランチを表示.
	where = branch --contains
	# git here : 現在のリポジトリのパスを取得. pwdコマンドに近いが表記がWindowsオリジナルに近くなる.
	here = rev-parse --show-toplevel
	#git showコマンドだと--onelineオプションが効かない気がするので、等価な働きをするlog -1を使う
	show2 = log -1

	# git show-file <コミットハッシュorブランチ名> <ファイルパス>
    # 詳細はこちら(https://qiita.com/YamEiR/items/7002ec1869717b7da9ce)
	# 特定の時点(コミット/ブランチ等)の特定ファイルをエディタで開く.
	# 作業ディレクトリは(Windowsの場合)AppData/Local/Temp/git-show/ であり、1週間以上経過すると削除される.
    # git config core.editorに適当なエディタを設定していないと動作しないため注意.
	show-file = "!f(){ \
		if [ $# -eq 2 ];then \
			local repo=$(git rev-parse --short ${1});\
			if [ "${?}" -ne 0 ]; then \
				return 1;\
			fi;\
			git ls-files ${2} > /dev/null || return 1 ; \
			local f_path=${2};\
			local f_name=${f_path##*/};\
			local f_row_name=${f_name%.*};\
			local f_ext=${f_name##*.};\
			mkdir -p /tmp/git-show;\
			git show ${repo}:${f_path} > /tmp/git-show/${f_row_name}-${repo}.${f_ext};\
			if [ $? -eq 0 ];then \
				$(git config core.editor) /tmp/git-show/${f_row_name}-${repo}.${f_ext} &\
			fi;\
		else \
			echo \"syntax error : git show-file <repo-name> <file-path>\";\
		fi;\
		find /tmp/git-show/* -mtime +7 -exec rm {} + ;\
	};f"
	
    # git pull-o ブランチ名
    # 詳細はこちら(https://qiita.com/YamEiR/items/630fcdd3c4c1984fdc2a)
    # 任意のブランチがfast-forward可能な場合にpullする
	pull-o = "!f(){ \
		local branch_name=${1};\
		local current_branch=$(git symbolic-ref --short HEAD);\
		local branch_remote=$(git config branch.${branch_name}.remote);\
		local branch_path=$(git config branch.${branch_name}.merge);\
		if [ ${branch_name} = ${current_branch} ];then \
			git pull ${branch_remote} ${branch_name} --ff-only;\
			return ${?};\
		else \
			git fetch ${branch_remote} && \
			git fetch . refs/remotes/${branch_remote}/${branch_name}:${branch_path};\
			return ${?};\
		fi;\
	};f"
```