Paper Plane xUI(PPx)をgitのフロントエンドとして使う

gitのフロントエンドツールといえばSourceTreeが有名です。
ですが僕の場合は[Git Extensions](https://sourceforge.net/projects/gitextensions/)をビューアとして使い、ブランチの編集にPPxを使っています。


# 画面構成

Git Extensionsでブランチ一覧ビューア(git log --graph相当)、PPxでファイル一覧とgit status結果ListFileの2ペイン構成を基本にしています。
![git編集画面.png](https://qiita-image-store.s3.amazonaws.com/0/51728/d20f3faa-2c31-9f31-3a42-36701150a527.png)


# デモ

![git_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/f1f5e1e1-38cb-147c-f7c3-2fd672f1ad67.gif)
Git ExtensionsのリロードはAutoHotkeyでグローバルホットキーを設定しています。

gitの操作をPPxで行うと、.gitconfigにコマンドの短縮目的のエイリアスがほぼ必要なくなります(たまにコマンドプロンプトから直接コマンド叩くので若干残ってますが)。
git statusは「Shift+g, s」、git addはエントリー選択して「Ctrl+g, a, Enter」、ブランチ一覧を見たければ「Shift+g, g」、diff見たければエントリー選択して「Ctrl+g, t, Enter」って感じにショットカットキーで操作出来ます、快適です。
ちなみにVimで開くのは「Shift+e」です。
*Ctrl+gとShift+gの違いは[後述](#ppx設定)*


# ツール構成

## gitst2listfile

git status結果をPPxで見るには、ListFileへと変換する専用のコマンドを作成しています。
[gitst2listfile](https://github.com/wordijp/gitst2listfile)
こちらのコマンドをインストールすれば、「Shift+g, s」が動作します。

## .gitconfig

たまに使うエイリアスの他に、PPxで使っているものもあります。
diff、mergeはご自身の環境へ合わせて下さい。

```.gitconfig
[alias]
	st = status
	co = checkout
	cod = checkout --detach
	br = branch
	; PPxで使用 ---
	; ブランチ一覧を最新更新順で
	branch-sort-date = !git for-each-ref --sort=-committerdate --format=\"%(refname:short)\" refs/heads/
	bsd = !git branch-sort-date
	; 最新ブランチ
	branch-latest = !git branch-sort-date | head -n 1 | tr -d '\n'
	bl = !git branch-latest
	; カレントのブランチ名
	branch-name = !git branch | grep -E '^\\*' | sed -E 's/^\\* //' | tr -d '\n'
	bn = !git branch-name
	; マージ済みブランチを削除
	delete-merged-branches = !git branch --merged | grep -v -E '^\\*|develop|master' | xargs git branch -d
	delmgbr = !git delete-merged-branches
	; --- PPxで使用
	; カレントのコミットタイトル
	title = !git log -1 --pretty=format:\"%s\"
	; コミットハッシュ、デフォルトはカレント
	revision = "!f() {                                \n\
		[ -n \"$1\" ] && target=\"$1\" || target=HEAD \n\
		git rev-parse --short $target | tr -d '\n' ;  \n\
	}; f"
	rev = !git revision
	; カレントのタグバージョン
	tagversion = describe --tags --abbrev=0
	tagv = !git tagversion
	; 指定数のコミットをrebase -i
	rih = "!f() { git rebase -i HEAD~$1; }; f"
	; ログ
	log-stat = log --stat --decorate=short --pretty=format:\"%C(yellow)%h %C(green)%cr %C(magenta)[%cn]%C(red)%d %C(reset)%s %C(cyan)%b\"
	ls = !git log-stat
	log-merges = log --merges --pretty=format:\"%C(yellow)%h %C(green)%ci %C(magenta)[%cn]%C(red)%d %C(reset)%s %C(cyan)%b\"
	lm = !git log-merges
	log-branch = log --stat --decorate=short --pretty=format:\"%d\"
	lb = !git log-branch
	; GitExtensionsで良い
	;log-graph= log --graph --date=short --decorate=short --pretty=format:\"%C(yellow)%h %C(green)%cd %C(magenta)[%cn]%C(red)%d %C(reset)%s\"
	;lg = !git log-graph
	;log-graph-all = !git log-graph --all
	;lga = !git log-graph-all
	; 指定時間以内のcommitを取得
	hour = "!f() {                                                                                                 \n\
		[ -n \"$1\" ] && since=\"$1\" || since=8                                                                   \n\
		echo \"since $since hour ago\"                                                                             \n\
		git log --oneline --since=$since' hour ago' --pretty=format:\"%C(yellow)%h %C(magenta)[%cn] %C(reset)%s\"; \n\
	}; f "
	h = !git hour
	today = !git hour 12
[diff]
	guitool = winmerge
	tool = winmerge
[difftool "winmerge"]
	path = E:/tool/favorite/WinMerge/WinMergeU.exe
	cmd = \"E:/tool/favorite/WinMerge/WinMergeU.exe\" -e -x -ub -wl -dl \"Base\" -fr -dr \"Mine\" \"$LOCAL\" \"$REMOTE\"
[merge]
	guitool = winmerge
	tool = winmerge
[mergetool "winmerge"]
	path = E:/tool/favorite/WinMerge/WinMergeU.exe
	cmd = \"E:/tool/favorite/WinMerge/WinMergeU.exe\" -e -x -ub -wl -dl \"Other\" -fm -dm \"Merge\" -wr -dr \"Mine\" \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o \"$MERGED\"
```

## スクリプト

外部コマンドの実行結果の標準出力の取得用に、exec.jsスクリプトを用意します、この記事の設定ではPPxディレクトリ/script/exec.jsを想定しています、ご自身の環境へ合わせて下さい。

```js:exec.js
//! script

//
// コマンド実行結果を返す
//

// コマンドを裏で実行し、標準出力/標準エラー出力を返す
function execSyncBackground(cmd) {
	// Runで実行し、結果をテキストでやりとりする
	var tmp_stdout = PPx.Extract("%'temp'" + '\\execsyncbackgroundstdout.tmp');
	var tmp_stderr = PPx.Extract("%'temp'" + '\\execsyncbackgroundstderr.tmp');

	var wshshell = new ActiveXObject("WScript.Shell");
	wshshell.Run('cmd /c ' + cmd + ' > ' + tmp_stdout + ' 2> ' + tmp_stderr, 0, true);

	var stdout, stderr;
	{
		var fs = new ActiveXObject("Scripting.FileSystemObject");
		var file_stdout = fs.OpenTextFile(tmp_stdout, 1);
		stdout = (!file_stdout.AtEndOfStream) ? file_stdout.ReadAll() : '';
		file_stdout.Close();

		var file_stderr = fs.OpenTextFile(tmp_stderr, 1);
		stderr = (!file_stderr.AtEndOfStream) ? file_stderr.ReadAll() : '';
		file_stderr.Close();
	}

	return [stdout, stderr];
}

///

var cmd = PPx.Arguments.item(0);
if (cmd == "") {
	result = "";
} else {
	result = execSyncBackground(cmd)[0];
}

PPx.Result = result;
```

2016/12/04追記:他にも、ListFileを開いた時だけエントリー一覧を読み込み順にしたい為、エントリー一覧を更新した時に呼ばれるLOADEVENTイベントでloadevent.jsを呼び出し、ListFileを開いた時だけ読み込み順にします、ついでにタブの色も変えています。
こちらもPPxディレクトリ/script/loadevent.jsを自身の環境へ合わせて下さい。

```js:loadevent.js
//!*script

//
// LOADEVENT処理
//

// ListFileの場合
var VFSDT_LFILE = 4; // 4:ListFile

if (PPx.DirectoryType == VFSDT_LFILE) {
	// 19: 読み込み順ソート
	PPx.Execute('*sortentry 19');
	// タブ色
	PPx.Execute('*pane color t,_BLA,_GRE');
} else {
	PPx.Execute('*pane color t,_AUTO,_AUTO');
}
```



## PPx設定

gitst2listfileの色分けは、出力されたListFileに専用拡張子を付けて、それに色を付けて対応しています。
Ctrl+Gがエントリーへの操作系、Shift+Gがブランチ、リモート、その他への操作系になっています。
checkoutやrmなど、取り返しのつかないコマンドは実行確認をするようにしています。
**※自身の設定が消えないように、適用前にバックアップを取るか、追加取り込みをしてください**
**※GitExtensionsやスクリプトのパスは自身の環境へと合わせて下さい**

2016/12/4追記:LOADEVENTについてを追加しました

```
A_exec	= {	; エイリアス
GitExtensions	= E:\tool\Developer\GitExtensions\GitExtensions.exe
}

C_ext	= {	; 拡張子色 gitst2listfileで使用
UNTRACKED	= _DMAG
COMMITTED	= _GRE
NOTSTAGED	= H0000C0
UNMERGED	= _RED
}

KC_main	= {	; PPcメイン窓
^G	,%M_menuCustomGit1
\G	,%M_menuCustomGit2
LOADEVENT	,*script %0\script\loadevent.js
}


M_menuCustomGit1	= {	** comment **
エントリーへの操作	= 
------------------	= 
git &add <FC>	= %Obsq git add %#
git &add -p <FC>	= %OBrsq git add -p %FC
git &blame <FC>(GitExtensions)	= %Ob %'GitExtensions' blame %FDC
git check&out <FC> <Q>	= 
	%"git checkout %FC(Enterで実行1/2)"%{%}
	%"git checkout %FC(Enterで実行2/2)"%{%}
	%Obsq git checkout %#
git check&out --theirs <FC> <Q>	= 
	%"git checkout --theirs %FC(Enterで実行1/2)"%{%}
	%"git checkout --theirs %FC(Enterで実行2/2)"%{%}
	%Obsq git checkout --theirs %FC
git check&out --ours <FC> <Q>	= 
	%"git checkout --ours %FC(Enterで実行1/2)"%{%}
	%"git checkout --ours %FC(Enterで実行2/2)"%{%}
	%Obsq git checkout --ours %FC
git diff&tool -t winmerge <FC>	= %Obrsq git difftool -t winmerge -y %FC
git merge&tool -t winmerge <FC>	= %Obrsq git mergetool -t winmerge -y %FC
git &reset HEAD <FC>	= %OBsq git reset HEAD %#
git r&m -r <FC> <Q>	= 
	%"git rm -r %FC(Enterで実行1/2)"%{%}
	%"git rm -r %FC(Enterで実行2/2)"%{%}
	%OBsq git rm -r %#
git m&v <FC> <moved>	= %OBrsq git mv %FC %"git mv src -> dst"%{new_name%}
}


M_menuCustomGit2	= {	** comment **
エントリー以外への操作	= 
----------------------	= 
git &status | gitst2listfile > listfile	= 
	*set LSTFILE=%'temp'\gitstlistfile.tmp
	%Obsq git status | gitst2listfile > %'LSTFILE'
	*jumppath %'LSTFILE'
open &GitExtensions	= %Ob %'GitExtensions' browse %*script(%0\script\exec.js, "git rev-parse --git-dir")
--	= 
git &branch <command>	= 
	*set ARG=%"git branch [-m|-d|-D] <branchname>(入力1/2)"%{%}
	%"git branch [-m|-d|-D] <branchname>(Enterで実行2/2)"%{%}
	%OB git branch %'ARG'
git check&out <target>	= 
	*set ARG=%"git checkout <target>(入力1/2)"%{.%}
	%"git checkout <target>(Enterで実行2/2)"%{%}
	%OB git checkout %'ARG'
git cherry-&pick <target>	= %OBsq git cherry-pick %"git cherry-pick <target>"%{target%}
git &commit	= %Ob git commit
git &commit --amend	= %Ob git commit --amend
git &commit --fixup'='<target>[~<N>]	= 
	*set CURRENT=%*script(%0\script\exec.js, "git branch-name")
	%OB git commit --fixup=%"git --fixup=<target>"%{%'CURRENT'~%|1%|%}
git flo&w feature start <command>	= %OB git flow feature start %"git flow feature start <branch>"%{hoge_branch%}
git flo&w feature publish <command>	= %OB git flow feature publish %"git flow feature publish <branch>"%{hoge_branch%}
git &tag -a <tagname>	= %OB git tag -a %"git tag -a <tagname>"%{newtag%}
git r&ebase <upstream> [<branch>]	= 
	*set dummy='よく対象になる最新ブランチをセットしておく'
	*set LATEST=%*script(%0\script\exec.js, "git branch-latest")
	%OB git rebase %"git rebase <upstream> [<branch>]"%{%'LATEST'%}
git r&ebase --onto <newbase> <upstream> <branch>	= 
	*set LATEST=%*script(%0\script\exec.js, "git branch-latest")
	%OB git rebase --onto %"git rebase --onto <newbase> <upstream> <branch>"%{%'LATEST'%}
git r&ebase -i HEAD~<N>	= %OB git rebase -i HEAD~%"git rebase -i HEAD~<N>"%{2%}
git r&ebase -i --autosquash HEAD~<N>	= %OB git rebase -i --autosquash HEAD~%"git rebase -i --autosquash HEAD~<N>"%{2%}
git &reset <target>[~<N>]	= 
	*set CURRENT=%*script(%0\script\exec.js, "git branch-name")
	%OB git reset %"git reset <target>"%{%'CURRENT'~%|1%|%}
git &reset HEAD~<N>	= %OB git reset HEAD~%"git reset HEAD~<N>"%{1%}
git m&erge [--no-ff] <commit>	= 
	*set dummy='よく対象になる最新ブランチをセットしておく'
	*set LATEST=%*script(%0\script\exec.js, "git branch-latest")
	%OB git merge %"git merge <commit>"%{--no-ff %|%'LATEST'%|%}
--	= 
git &delete-merged-branches <Q>	= 
	%"git delete-merged-branches(Enterで実行1/2)"%{%}
	%"git delete-merged-branches(Enterで実行2/2)"%{%}
	%OB git delete-merged-branches
git &fetch --prune	= %OB git fetch --prune
git &fetch	= %OB git fetch
git p&ush <command>	= %Obsq git push %"git push <branch>"%{origin %|master%|%}
}
```

# まとめ

gitの操作は体で覚えるもの