PowerShellでフォームを使ってgit diffコマンドをGUIツール化した

PowerShellで.NETのフォームが作れることを知ったので触ってみました。

# 作ったもの
Gitの差分を取得するgit diffコマンドと、ログを取得するgit logコマンドをボタンクリックで実行してテキストボックスに表示するフォームを作りました。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528583/f7d6cf6b-b6db-cb12-23ff-228356604882.gif)


## パーツ説明
- 構成
  - git_diff_tool.ps1・・・スクリプトファイル
  - param.json・・・設定ファイル

### パラメータ
![スクリーンショット 2021-07-15 200024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528583/58855573-6758-fbb9-6590-93a1f919e0c3.png)
Git管理されているプロジェクトのフォルダパスを入力する場所です。

  - Browse：フォルダの選択画面を開きます
  - Open：入力されているフォルダパスをエクスプローラーで開きます
  - Save：フォルダパスをjsonファイルに保存します。

### コミットID入力欄
![スクリーンショット 2021-07-15 200052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528583/9d175ccb-a38a-a1c8-2ed9-405571e3359b.png)
コミットIDを入力するテキストボックスです。
横のテキストボックスはgit diffのオプション(--diff-filter)になります。チェックのオンオフでそれぞれ表示非表示できます。

  - A：追加
  - C：コピー
  - M：修正
  - R：名前変更
  - D：削除

### 実行ボタン/結果表示
![スクリーンショット 2021-07-15 200132.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528583/69fedf68-3712-f7eb-0dc0-d01204ef71f6.png)
各コマンドの実行ボタンとその結果を表示するテキストボックスです。

# ソースコード
フォームのボタンクリックで各関数を呼び出します。
## gitdiff関数
「git diff」ボタン押下で実行される関数です。

### テキストボックス、チェックボックスの内容を変数に格納
``` powershell
# テキストボックスの内容を変数に格納
$proj = $InputBoxProj.Text
# プロジェクトフォルダをカレントディレクトリに設定
Set-Location $proj

# BeforeコミットID
$commit_from = $InputBox.Text
# AfterコミットID
$commit_to = $InputBox2.Text

# チェックボックスの内容を取得
if ($ChecktBoxA.Checked -eq $True){
    $filter += "A"
}
if ($ChecktBoxC.Checked -eq $True){
    $filter += "C"
}
if ($ChecktBoxM.Checked -eq $True){
    $filter += "M"
}
if ($ChecktBoxR.Checked -eq $True){
    $filter += "R"
}
if ($ChecktBoxD.Checked -eq $True){
    $filter += "D"
}
```

### git diffコマンドを実行
git diffコマンドを実行します。
エンコードなしだと実行結果が文字化けしてしまいます。
参考：[PowerShell処理でgitログが文字化けする](https://ja.stackoverflow.com/questions/53061/powershell%E5%87%A6%E7%90%86%E3%81%A7git%E3%83%AD%E3%82%B0%E3%81%8C%E6%96%87%E5%AD%97%E5%8C%96%E3%81%91%E3%81%99%E3%82%8B)

`--stat`で変更ステータスを取得、`--diff-filter=ACMRD`でコミットファイルのフィルタリングです。

``` powershell
$enc = [Console]::OutputEncoding
try
{
    [Console]::OutputEncoding = [Text.Encoding]::UTF8
    $outputBox.Text = git diff --stat --diff-filter=$($filter) $commit_from　$commit_to | Out-String } 
finally
{
    [Console]::OutputEncoding = $enc
}
```

## git log関数
「git log」ボタン押下で実行される関数です。
`--graph`でツリー構造に、`--name-status`で変更されたファイル情報を表示します。

```powershell
$proj = $InputBoxProj.Text

# プロジェクトフォルダをカレントディレクトリに
Set-Location $proj

$enc = [Console]::OutputEncoding
try
{
    [Console]::OutputEncoding = [Text.Encoding]::UTF8
    $result = git log -50 --graph --name-status | Out-String
}
finally
{
    [Console]::OutputEncoding = $enc
}

$OutputBox2.Text = $result
```

## jsonファイルの読み込み
プロジェクトのフォルダパスをスクリプトを閉じても保持できるようにjsonファイルに保存します。現状1つしかデータが無いので、わざわざjsonにする意味も薄いですが、今後の拡張にも対応しやすいのでこの形にしてみました。

```powershell
# jsonファイルの内容を取得
$json = Get-Content param.json | ConvertFrom-Json
$proj = $json.proj # プロジェクトフォルダ
```

```json:param.json
{
    "proj":  "C:\\ProjectFolder"
}
```


## jsonファイルの書き込み関数
「Save」ボタン押下で実行される関数です。テキストボックスに入力されているフォルダパスをjsonファイルに上書きします。

```powershell
# テキストボックスの内容をjsonのフォーマットで変数に格納
$json = @{
    "proj" = $InputBoxProj.Text
}
# jsonファイルに出力
ConvertTo-Json $json | Out-File param.json -Encoding utf8
# メッセージボックスを表示
[System.Windows.Forms.MessageBox]::Show('Saved to "param.json".')
```

## フォルダ選択ダイアログ関数
「Open」ボタン押下で実行される関数です。
フォルダ選択のダイアログを表示し、選択されたフォルダのパスをテキストボックスに格納します。
この関数は下記サイトのほぼそのまま流用させていただいてます。
[Windows Form を作成してみよう(3/4)](http://kamifuji.dyndns.org/PS-Support/Form03/index.html)

```powershell
# COMオブジェクトの読み込み
$shell = New-Object -com Shell.Application

# ダイアログを表示し、結果を変数folderPathに格納する
$folderPath = $shell.BrowseForFolder(0,"対象フォルダーを選択してください",0,"C:\")

# キャンセルを選択した場合は終了
if ($folderPath -eq $Null){return}

# $folderPath内の情報のうち、パス情報のみを格納する
$InputBoxProj.text = $folderPath.Self.Path

# 格納したパスをメッセージボックスで表示
Add-Type -Assembly System.Windows.Forms
[System.Windows.Forms.MessageBox]::Show($folderPath.Self.Path,"処理完了")
```

## フォーム定義
PowerShellでフォームを定義するには座標など全て手打ちで作成する必要があります。GUI的に位置調整できないのは少し難点・・・。

### アセンブリの読み込み
```powershell
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
```

### フォント
ラベルとテキストボックスのフォントを指定します。

```powershell
$Font = New-Object System.Drawing.Font("ＭＳ ゴシック",11)       # ラベルのフォント
$FontTextbox = New-Object System.Drawing.Font("ＭＳ ゴシック",9) # テキストボックスのフォント
```

### フォーム定義
```powershell
$Form = New-Object System.Windows.Forms.Form -Property @{
    Size = "1095,810"
    Text = "git_diff_tool"
    Add_Shown = {$InputBox.Select()} # FromコミットIDテキストボックスにフォーカス
    StartPosition = "CenterScreen"   # 中心に表示する
    #StartPosition = "Manual"        # 表示位置を指定する
    #Location = "0,200"              # 座標
    #Opacity = 0.9                   # 透明度
    #Topmost = $True                 # 最前面固定
}
```

### パラメータ

```powershell
# パラメータグループ
$MyGroupBox = New-Object System.Windows.Forms.GroupBox -Property @{
    Location = "10,10"
    size = "700,50"
    text = "Parameter"
}

# プロジェクトフォルダラベル
$CommitLabel = New-Object System.Windows.Forms.Label -Property @{
    Location = "10,20"
    Size = "120,20"
    Text = "Project Folder:"
    Forecolor = "black"
    Font = $Font
}
$Form.Controls.Add($CommitLabel)

# プロジェクトフォルダテキストボックス
$InputBoxProj = New-Object System.Windows.Forms.TextBox -Property @{
    Location = "130,20"
    Size = "400,20"
    Text = $proj
}
$Form.Controls.Add($InputBoxProj)

# フォルダ選択ボタン
$Button_folder = New-Object System.Windows.Forms.Button -Property @{
    Location = "540,30"
    Size = "55,20"
    Text = "Browse"
    Add_Click = {Folder-Select} # 関数呼び出し
}
$Form.Controls.Add($Button_folder)

# フォルダを開くボタン
$Button_open = New-Object System.Windows.Forms.Button -Property @{
    Location = "590,30"
    Size = "55,20"
    Text = "Open"
    Add_Click = {Invoke-item $InputBoxProj.Text}
}
$Form.Controls.Add($Button_open)

# パラメータ保存ボタン
$Button_parm = New-Object System.Windows.Forms.Button -Property @{
    Location = "645,30"
    Size = "55,20"
    Text = "Save"
    Add_Click = {Save-Param}
}
$Form.Controls.Add($Button_parm)

# グループに入れる
$MyGroupBox.Controls.AddRange(@($CommitLabel,$InputBoxProj,$InputBoxCpfolder))
$Form.Controls.AddRange(@($MyGroupBox))
```

### コミットID入力欄

```powershell
# BeforeコミットID入力欄
$InputBox = New-Object System.Windows.Forms.TextBox -Property @{
    Location = "60,80"
    Size = "380,20"
}
$Form.Controls.Add($InputBox)

# AfterコミットID入力欄
$InputBox2 = New-Object System.Windows.Forms.TextBox -Property @{
    Location = "60,100"
    Size = "380,20"
    Text = "master"
}
$Form.Controls.Add($InputBox2)
```

### 実行ボタン、チェックボックス

```powershell
# 実行ボタン
$Button = New-Object System.Windows.Forms.Button -Property @{
    Location = "540,80"
    Size = "100,40"
    Text = "git diff"
    Font = $font
    BackColor = "#add8e6"
    Add_Click = {Git-Diff} # 関数呼び出し
}
$Form.Controls.Add($Button)
$Form.AcceptButton = $Button # Enterキーでgit diffを実行

# 実行ボタン2
$Button2 = New-Object System.Windows.Forms.Button -Property @{
    Location = "640,80"
    Size = "100,40"
    Text = "git log"
    Font = $font
    BackColor = "#add8e6"
    Add_Click = {Git-Log} # 関数呼び出し
}
$Form.Controls.Add($Button2)

# チェックボックス
$ChecktBoxA = New-Object System.Windows.Forms.CheckBox -Property @{
    Location = "445,80"
    Size     = "30,20"
    Text     = "A"
    Checked  = $True
}
$Form.Controls.Add($ChecktBoxA)

$ChecktBoxC = New-Object System.Windows.Forms.CheckBox -Property @{
    Location = "445,100"
    Size     = "30,20"
    Text     = "C"
    Checked  = $True
}
$Form.Controls.Add($ChecktBoxC)

$ChecktBoxM = New-Object System.Windows.Forms.CheckBox -Property @{
    Location = "480,80"
    Size     = "30,20"
    Text     = "M"
    Checked  = $True
}
$Form.Controls.Add($ChecktBoxM)

$ChecktBoxR = New-Object System.Windows.Forms.CheckBox -Property @{
    Location = "480,100"
    Size     = "30,20"
    Text     = "R"
    Checked  = $True
}
$Form.Controls.Add($ChecktBoxR)

$ChecktBoxD = New-Object System.Windows.Forms.CheckBox -Property @{
    Location = "510,80"
    Size     = "30,20"
    Text     = "D"
    Checked  = $True
}
$Form.Controls.Add($ChecktBoxD)

# テキストラベル(from)
$CommitLabel = New-Object System.Windows.Forms.Label -Property @{
    Location = "10,80"
    Size     = "60,20"
    Text     = "From"
    Forecolor = "black"
    Font      = $Font
}
$Form.Controls.Add($CommitLabel)

# テキストラベル(to)
$CommitLabel2 = New-Object System.Windows.Forms.Label -Property @{
    Location = "10,100"
    Size     = "60,20"
    Text     = "To"
    Forecolor = "black"
    Font      = $Font
}
$Form.Controls.Add($CommitLabel2)
```

### 出力結果表示テキストボックス

```powershell
# 出力結果
$OutputBox = New-Object System.Windows.Forms.TextBox -Property @{
    Location = "10,140"
    Size = "530,600"
    MultiLine = $True
    ScrollBars = "Vertical"
    Font = $FontTextbox
    text = "<git diff>"
    ReadOnly = $True # 読み取り専用
}
$Form.Controls.Add($outputBox)

# 出力結果2
$OutputBox2 = New-Object System.Windows.Forms.TextBox -Property @{
    Location = "540,140"
    Size = "530,600"
    MultiLine = $True
    ScrollBars = "Vertical"
    Font = $FontTextbox
    ReadOnly = $True
    text = "<git log>"
}
$Form.Controls.Add($OutputBox2)
```

### Escキーでフォームを閉じる
Escキー押下でフォームを閉じるようにします。

```powershell
# キャンセルボタン(フォームには配置しない)
$CancelButton = New-Object System.Windows.Forms.Button -Property @{
    Size = "0,0"
    DialogResult = "Cancel" # フォームを閉じる
}
$form.Controls.Add($CancelButton)

# ESCキー押下でキャンセルボタンを実行
$Form.CancelButton = $CancelButton
```


# おわりに
PowerShellでフォームを作成してみました。
スクリプトはGithubで公開しています。他に良い書き方あるよ！って方はお気軽にプルリクもしくはコメントいただけますと幸いです。
https://github.com/yt3trees/git_diff_tool

各オブジェクトの位置調整は手入力する必要があるので、GUIツールをがっつり作り込むには向いていませんが、手軽に作れて良いですね。
ping等のコマンドを良い感じのGUIツール化するのに良い選択肢かもしれません。

基本的にWindowsであれば環境構築せずに実行できると思うので、他メンバーに共有しやすいのもプラスポイント。

もう少し遊べそうです。

以上