gitの共有リポジトリにpushしたらSlackとChatworkにメッセージを通知する

## 追記

2017/10/02 ライブラリを `HTTP::Tiny` に変更しました。
2017/03/09 Slack APIの変更に追随するのとあわせて、文面を更新しました。
2014/07/21 スクリプトを全面的に書き直し、文面を追記／修正しました。

## 目的

以前、「[gitの共有リポジトリにpushしたらChatworkにメッセージを通知する](http://qiita.com/artifactsauce/items/814b6c8a94e4f35dcf9e)」という記事を書いたのですが、最近になって、チャットツールに[Slack](https://slack.com/)も使うようになったので、同じ目的でgit push時にどちらにも通知を送ることにします。

## bashスクリプトをperlで書きなおした理由

SlackのAPIはPOSTパラメーターにJSONを埋め込むという形式になっています。Chatworkだけに通知を送っていた時は`bash`を使っていたのですが、`bash`ではJSONの取扱いに難があったので、`perl`を使って全面的に書き換えることにしました。`perl`を採用した最も大きな理由は私が一番慣れている言語ということですが、多くのLinuxシステムに標準でインストールされているということも理由の一つです。

## Slack Incoming WebHooks

Slackには多くの魅力がありますが、Integrationのための機能が非常に充実しているということが最も大きいと個人的には思います。既存の有名サービスから簡単に通知を送ることができるのはもちろん、Incoming Webhooksを用いて、自作ツールの中にSlackへの通知機能を簡単に実装することができます。今回はこのIncoming Webhooksを用いてgitリポジトリから通知を送ることにします。

詳細は下記の公式APIドキュメントをお読みください。

- https://my.slack.com/apps
- https://api.slack.com/

## post-update

おおよその内容の理解は[前述の記事](http://qiita.com/artifactsauce/items/814b6c8a94e4f35dcf9e)を読んでいただきたいと思います。

作成されたhooksスクリプトは下記のとおりです。不必要なほど重厚な作りになってしまいましたね。
ムシャクシャしてやった。後悔はしていない。

```pl:post-update
#!/usr/bin/env perl

package MyApp::Notifier::Base;

use warnings;
use HTTP::Tiny;

sub new {
    my $class = shift;
    my $self = { @_ };
    bless( $self, $class );
    return $self;
}

sub send {
    my $self = shift;
    my ( $repos, $branch, $message ) = @_;
    my ( $url, $post_data, $headers ) = $self->_prepare_request_parameters( $repos, $branch, $message );

    my $ua = HTTP::Tiny->new();
    my $response = $ua->post($url, {
        'headers' => $headers,
        'content' => $post_data,
    } );
    unless ($response->{success}) {
        warn $response->{reason};
        return 0;
    }
    return 1;
}

sub _prepare_request_parameters {
    die "[ERROR] Have to override this method";
}

package MyApp::Notifier::Slack;

use warnings;
use JSON::PP;

@MyApp::Notifier::Slack::ISA = qw(MyApp::Notifier::Base);

sub _prepare_request_parameters {
    my $self = shift;
    my ( $repos, $branch, $message ) = @_;

    my $endpoint    = "https://hooks.slack.com/services";
    my $path_token  = $self->{path_token};
    my $channel     = $self->{channel};
    my $username    = 'Git hooks';
    my $icon        = ':jack_o_lantern:';

    my $text = "(\`${repos}\`) [\`${branch}\`]";

    if ( $branch eq 'master' ) {
        $text = ":bell: PUSHED INTO \`${branch}\` BRANCH!!!\n${text} ${message}";
    }
    elsif ( ! -f "refs/heads/$branch" ) {
        $text = "${text} Branch was deleted.";
    }
    else {
        $text = "${text} ${message}";
    }

    my $headers = {
        'Content-Type' => 'application/x-www-form-urlencoded',
    };

    my $post_data = "payload=" . encode_json +{
        channel => $channel,
        username => $username,
        icon_emoji => $icon,
        text => $text,
    };

    return "$endpoint/$path_token", $post_data, $headers;
}

package MyApp::Notifier::Chatwork;

use warnings;

@MyApp::Notifier::Chatwork::ISA = qw(MyApp::Notifier::Base);

sub _prepare_request_parameters {
    my $self = shift;
    my ( $repos, $branch, $message ) = @_;

    my $endpoint = "https://api.chatwork.com/v1/rooms";
    my $token    = $self->{token};
    my $room_id  = $self->{room_id};

    my $url  = "$endpoint/$room_id/messages";
    my $text = "(${repos}) [${branch}] ${message}";
    if ( $branch eq 'master') {
        $text = "[info][title]PUSHED INTO \`master\` BRANCH!!![/title]${text}[/info]";
    }

    my $post_data = 'body=' . $text;

    my $headers = {
        'X-ChatWorkToken' => $token,
    };

    return $url, $post_data, $headers;
}

package MyApp::Git;

use warnings;
use FindBin qw/$Bin/;
use File::Basename qw/dirname/;
use Encode;
use Encode::Guess qw/shift-jis euc-jp 7bit-jis/;

sub new {
    my $class = shift;
    my $self = { @_ };
    bless( $self, $class );
    return $self;
}

sub repos_name {
    my $self = shift;
    unless ( defined $self->{repos_name} ) {
        my $dir = (split( /\//, dirname( $Bin ) ))[-1];
        $dir =~s/\.git$//;
        $self->{repos_name} = $dir;
    }
    return $self->{repos_name};
}

sub branch_name {
    my $self = shift;
    unless ( defined $self->{branch_name} ) {
        $self->{branch_name} = `git rev-parse --symbolic --abbrev-ref $_[0] 2> /dev/null`;
        chomp $self->{branch_name};
    }
    return $self->{branch_name};
}

sub commit_message {
    my $self = shift;
    my $branch = shift;
    unless ( defined $self->{commit_message} ) {
        $self->{commit_message} = `git log -1 --pretty=format:"%h - %an : %s" $branch 2> /dev/null`;
    }
    return unless $self->{commit_message};
    my $decoder = Encode::Guess->guess( $self->{commit_message} );
    if ( ref $decoder ) {
        $self->{commit_message} = $decoder->decode( $self->{commit_message} );
    }
    return $self->{commit_message};
}

package main;

use warnings;
use JSON::PP;

scalar @ARGV == 0 && die "[ERROR] Arguments not enough";

my $git     = MyApp::Git->new();
my $repos   = $git->repos_name();
my $branch  = $git->branch_name( @ARGV );
my $message = $git->commit_message( $branch );

my %notifiers = (
    'slack' => sub { new MyApp::Notifier::Slack(@_) },
    'chatwork' => sub { new MyApp::Notifier::Chaktwork(@_) },
);

my $config  = _load_config();

while ( my ( $name, $generator ) = each %notifiers ) {
    my $notifier = $generator->( %{$config->{$name}} );
    $notifier->send( $repos, $branch, $message )
        || warn "[WARN] Faild to send message.";
}

exit;

sub _load_config {
    return decode_json( join '', <DATA> );
}

1; # magic number

__DATA__
{
  "slack": {
    "path_token": "<CHANGE_THIS>",
    "channel": "<CHANGE_THIS>"
  },
  "chatwork": {
    "token": "<CHANGE_THIS>",
    "room_id": "<CHANGE_THIS>"
  }
}
```

最新のバージョンは[gist](https://gist.github.com/artifactsauce/870ba1486d0fe2acad80)に置いてありますので、興味のある方は是非どうぞ。

下記の環境で動作確認しています。

OS: macOS Sierra 10.12.3（16D32）
Perl: v5.20.3 (via plenv)

できるだけ標準な環境で動作するように考えましたが、Perlのライブラリを１つだけインストールする必要があります。

```console
$ cpanm IO::Socket::SSL
```

通知対象としたいすべてのリモートリポジトリの `$REPOSITORY_ROOT/hooks/` に `post-update`という名前で前述のファイルを置きます。実行権限をつけることも忘れないでください。

```console
$ vi post-update # ファイル末尾の設定を変更
$ chmod 755 post-update
$ scp ./post-update $REMOTE_HOST:$REPOSITORY_ROOT/hooks/
```

`$REPOSITORY_ROOT`と`$REMOTE_HOST`はそれぞれ、リポジトリの最上位ディレクトリとリモートリポジトリを置いてあるホストの名前を示す任意の値とします。

次項以降でこのスクリプトの内容を解説します。

### 構成

１つのファイルですが、４つのpackageに分かれています。このスクリプトの実行時に呼び出されるのは `main` パッケージです。その他はgitのコミットの情報などを取得するモジュールやチャットに送信するモジュールです。送信する機能は `MyApp::Notifier::Base` に記載されており、このモジュールを継承して通知先サービスごとの送信モジュールを作成します。これらのモジュールは、リクエストパラメーターを作成するサブルーチン`_prepare_request_parameters`を上書きします。

### 設定値

編集すべき設定値は、スクリプトの最後、`__DATA__`の下に記載されたJSONにまとめられています。ご自分の環境に合わせて編集してください。

```json
{
  "slack": {
    "path_token": "hogehoge/hogehoge/hogehoge",
    "channel": "general"
  },
  "chatwork": {
    "token": "hogehogehoge",
    "room_id": "12345678"
  }
}
```

上記の値に何を設定すべきかは、各サービスの設定画面から取得してください。Slackの場合は、前述のIntegrationsのページでIncoming Webhooksを選択し、画面の指示通りに簡単に作成することができます。

### 投稿テキストの作成

投稿するメッセージは、前述の `_prepare_request_parameters()` と `commit_message()` というメソッドの中で構築しています。

`commit_message()` の中では `git log` コマンドを使ってコミットメッセージを取得しています。コミットメッセージは指定された（pushされた）ブランチの最後のものを利用します。取得する際にはメッセージのエンコーディングを判定してデコードしておきます。

```pl
sub commit_message {
    my $self = shift;
    my $branch = shift;
    unless ( defined $self->{commit_message} ) {
        $self->{commit_message} = `git log -1 --pretty=format:"%h - %an : %s" $branch 2> /dev/null`;
    }
    return unless $self->{commit_message};
    my $decoder = Encode::Guess->guess( $self->{commit_message} );
    if ( ref $decoder ) {
        $self->{commit_message} = $decoder->decode( $self->{commit_message} );
    }
    return $self->{commit_message};
}
```

次に `_prepare_request_parameters()` の中で、プラットフォーム合わせた形式にに加工します。Slackでは次のようにしています。

```pl
my $text = "(\`${repo}\`) [\`${branch}\`] ${message}";
if ( $branch eq 'master' ) {
    $text = ":bell: PUSHED INTO \`master\` BRANCH!!!\n$text";
}
```

リポジトリ名やブランチ名を入れることをおすすめします。 `master` ブランチにpushしたときには、より強めの注意喚起が必要でしょうから、さらに加工するようにしています。

Slackに関してはさらに、 `username` と `icon_emoji` は任意に設定することができます。 `icon_emoji` は[Emoji Cheet Sheet](http://www.emoji-cheat-sheet.com/)に記載されているものが使える他、下記のページ（参考URL）で自分で作成したアイコンを利用することもできます。

- Custom Emoji - https://my.slack.com/customize/emoji

`icon_emoji` は本文中でも使えます。この辺りは自由に創造性を持って編集していただければと思います。ちなみに、Qiitaでも表示できますよ :japanese_goblin:

### 投稿

実際に投稿してみます。

![git-push-notifies.png](https://qiita-image-store.s3.amazonaws.com/0/5823/f1367a6f-38c4-5cb5-86b2-d37c0779c7c5.png)

ちゃんと動作したでしょうか？