ディレクトリを定期的に監視してGitリポジトリに間違ったEmailをコミットしない

# はじめに

皆さんはGitリポジトリに対して間違ったEmailでコミットしたことはありませんか？僕はあります。

私の場合、仕事用と個人用でGitHubのアカウントが分かれており、仕事用のアカウントに対しては会社のメールアドレスを、個人用のアカウントにはGmailのメールアドレスを登録しています。フリーランスの方などでこのようにアカウントを分離している方を何人か見かけたことがあるので、わりと一般的な状況なのではないでしょうか？

`git commit` した際には、コミッターとして自分のEmailアドレスが保存されます。通常は `$HOME/.gitconfig` に書き込まれているデフォルトの値が保存されます。私の場合はGmailのメールアドレスを設定しています。

このような状況では、会社のリポジトリに対して下記のようにリポジトリ毎の設定を施します。

```console
$ git config user.email company-email@example.com
```

この作業を行っておかないと、会社のリポジトリにGmailのメールアドレスでコミットされ、個人用アカウントのアイコンがコミット履歴に表示されてしまいます。これは恥ずかしい。僕は実際にやったことがありますし、やはり何人かの方がやっているのを見たことがあります。

このような恥ずかしいミスを防止する方法はないのでしょうか？

# 方針

## 自動的に変更する

「気をつけよう」で解決すれば、そもそもこんな問題は起こりません。人間の注意力に依存せず、プログラムによって自動的に変更されるべきでしょう。そこで、監視プログラムを常駐プロセスとして起動させ、このプログラムが自動的にメールアドレスを変更させることにします。

## ディレクトリの変更を監視する

`git clone` または `git init` した際には、その作業ディレクトリの状態に変化が現れるはずです。そこで、ディレクトリに変更があった際にはリポジトリを作成している可能性が非常に高いと仮定し、対象ディレクトリの変化を常に監視して、変化があった際にリポジトリに対してメールアドレスを変更する処理を実行することにします。

## ディレクトリを限定する

リポジトリの作成を検出することができたとして、そのリポジトリが仕事用か個人用かを自動的に区別する方法はあるのでしょうか？リモートリポジトリが仕事用のものなので `git clone` の際には区別がつきますが、 `git init` の際には区別がつきません。やはりローカルの状況で区別をつけるしかありません。そこで仕事用のリポジトリは特定のディレクトリ直下に配置するという制限を加えることにします。そのディレクトリ配下には個人用のリポジトリが無いことを前提とします。

## 偽陽性は受け入れる

前項のような基準では、無駄な処理が実行されそうです。例えばその特定のディレクトリ直下に通常のファイルを作成しただけでも、ディレクトリの変更として検知されます。また配下にある複数のリポジトリのどれが新たに作成／クローンされたものかを判定するは面倒です。ディレクトリの変更を検知するという方法を採用する限り、誤検知は頻発しそうです。

そこで、対象とする挙動かどうかをその都度判断させてできるだけ無駄な処理が走らないようにします。その上で、最終的に何度実行されても同じ結果になるようにすることで、本質的には関係がない変更の検知があったとして処理が実行されても問題がないように実装します。

# 実装

そんなこんなで、下記のようなスクリプトを実装しました。

```bash:watch-git-email.sh
#!/usr/bin/env bash

set -eu

: ${DEBUG_MODE:=0}
: ${INTERVAL:=1} #監視間隔, 秒で指定

_.error() {
  echo -e "[$(date +"%x %T")] \033[35m[ERROR] $*\033[m" >&2
}

_.debug() {
  [ "$DEBUG_MODE" -eq 0 ] && return 0
  echo -e "[$(date +"%x %T")] \033[34m[DEBUG] $*\033[m" >&2
}

_.get_stamp() {
  ls -l --full-time $1 | awk '{print $1 $6 $7 $8 $9}' | openssl sha
}

_.run() {
  local target_directory=$1
  local expect_email=$2
  _.debug "Find in $target_directory"
  while read; do
    _.debug "Change directory: $REPLY"
    cd $(dirname $REPLY)
    local actual_email=$(git config user.email)
    if [ "$expect_email" = "$actual_email" ]; then
      continue
    fi
    _.debug "Change git user emal in: $REPLY"
    git config user.email $expect_email
  done <<EOS
$(find $target_directory -type d -name ".git")
EOS
}

_.watch() {
  local target_directory=$1
  local last=$(_.get_stamp $target_directory)
  while true; do
    sleep $INTERVAL
    local current=$(_.get_stamp $target_directory)
    _.debug "Stamp: $current"
    if [ "$last" = "$current" ]; then
        continue
    fi
    _.run $*
    last=$current
  done
}

if [ $# -ne 2 ]; then
  _.error "not enough arguments"
  exit 1
fi

TARGET_DIRECTORY=$1
EXPECT_EMAIL=$2

_.watch $TARGET_DIRECTORY $EXPECT_EMAIL
```

コピペで良いので適当なところに保存しましょう。

## 実行

下記のように実行します。スクリプトには実行権限を付けておいてください。

```console
$ watch-git-email.sh /PATH/TO/DIRECTORY/ROOT expect-email@example.com &
```

これでスクリプトのプロセスが常駐します。

## 停止

このプロセスを停止したい場合は下記のように実行します。

```console
$ pkill -KILL -f　watch-git-email.sh
```

# 解説

## 定期的な実行

定期的に実行するというロジックは端的に表現すると下記のとおりです。

```bash
while true; do
    sleep $INTERVAL
    # something to do
done
```

無限ループの中に一定時間の停止が含まれています。

今回のスクリプト中ではデフォルトでは１秒毎にディレクトリの変更を検知する処理を実行しています。変更があったときのみこの後の処理を行います。

## ディレクトリの変更の検知

ディレクトリの変更の検知するためには、現在の状態の識別し、一定時間の前後でこれが変化しているか否かで判断します。

この「現状の識別」は `_.get_stamp()` 関数にまとめられています。その内容は非常にシンプルで、 `ls -l` の実行結果の特定の項目を取得したものです。 `ls -l` の出力内容をすべて使うと誤検知が多くなってしまうため、検知に不要な項目は除外しています。

また一方で、 `ls -l` の出力内容だけでは１分以内の変化を検知できない場合あるため、 `--full-time` という最終変更日時を秒単位まで表示してくれるオプションを加えています。検知できない変更とは、例えば下記のような手順を１分以内に行った場合のことを指しています。

```console
$ rm -rf .git
$ git init
```

上記のコマンドを１分以内に行った場合、 `user.email` の値はデフォルトのものになっていますが、 `ls -l` の出力内容に変化が無いので検知できず、該当値の変更処理が実行されません。現状のロジックでも１秒以内に実行されると検知できないのですが、マニュアルでは不可能な動作なので考慮する必要はないでしょう。

今回は、目視で変更を検知する際（デバッグのこと）に比較しやすいと言う理由で、出力文字列のハッシュ値をとっています。

## ログ出力

開発中も然ることながら、実際に運用する際にも意図したとおりに動作しているのか不安になります。そんなときはログを出力するのが正攻法です。このスクリプトは地味にログをキレイに出すようにしています。

スクリプト中では下記のように記述されている箇所です。

```bash
echo -e "[$(date +"%x %T")] \033[35m[ERROR] $*\033[m" >&2
```

`echo` は標準出力に引数を表示するコマンドで、 `-e` オプションをを付けるとエスケープ文字を解釈してくれます。解釈してくれるその文字が上記の場合はエスケープシークエンス（ターミナルを制御する制御文字をあらわす）を記述するための "エスケープ" を示しているなのでちょっとこんがらがります。細かいことはともかく、上記の記述で`\033[35m` と `\033[m` に挟まれた部分の文字色がマゼンタ色で表示されます。 `35` という数値が文字色マゼンタで表示するということを指示しています。

環境変数 `DEBUG_MODE` を `1` にしておくと、DEBUGログが表示されるので試してみてください。

# 終わりに

これで仕事用のリポジトリに個人用のメールアドレスをコミットするという恥ずかしい事故を防ぐことができるようになりました。私ももういい大人なので、恥ずかしくない人生を歩みたいものです。