DroneCIでRubocopを導入した話

## やってこと
1. ファイルの差分だけでrubocopエラーを通知する
2. rubocopエラーはGithubAPI経由でGithubで投稿する
3. 上のロジックをDroneCIに持っていく

**最後**
 プルリクでrubocopエラーが起こるファイルの行ごとでエラー内容を投稿する (comment inline)

## 1.ファイルの差分だけでrubocopエラーを通知する
 まずは、3つのgemを導入する

```cmd:Gemfile
　　gem 'checkstyle_filter-git'
　　gem 'rubocop-select'
　　gem 'rubocop-checkstyle_formatter'
```
run_diffcop.shを作成する

```sh:run_diffcop.sh
err=$( git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err
```
`git fetch upstream master`の`upstream`はプロジェクトのrepo

### ローカルで試してみる

テストのファイルを作る

```ruby:test_rubocop.rb
class TestRubocop
  def test
    puts "test"
  end
end
```
`git add`と`git commit`しておく
次は、

```cmd
% sh run_diffcop.sh
<?xml version='1.0'?> 
<checkstyle> 
<file name='test_rubocop.rb'> 
<error column='0' line='1' message='Missing top-level class documentation comment.' severity='info' source='com.puppycrawl.tools.checkstyle.Style/Documentation'/> 
<error column='0' line='1' message='Missing magic comment `# frozen_string_literal: true`.' severity='info' source='com.puppycrawl.tools.checkstyle.Style/FrozenStringLiteralComment'/>
<error column='9' line='3' message='Prefer single-quoted strings when you don&apos;t need string interpolation or special symbols.' severity='info' source='com.puppycrawl.tools.checkstyle.Style/StringLiterals'/> 
</file>
</checkstyle>

```

ここまでは一旦xmlフォマットでエラーが出力できた
見やすくするため、parserを書く

```ruby:parse_rubocop_xml.rb
require 'nokogiri'
require "cgi"
xml_doc = Nokogiri::XML(ARGV.join(" "))
error_messages = ""
xml_doc.xpath("//file").each do |elm|
  begin
    elm.children.search("error").each do |error|
      error_messages << "- #{elm.attributes["name"].value}:#{error.attributes["line"].value}: #{error.attributes["message"].value}\\n"
    end
  rescue => e
    next
  end
end
puts CGI::escapeHTML(error_messages)

```
上のrun_diffcop.shを追加する

```sh:run_diffcop.sh
err=$( git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err

mess=`bundle exec ruby parse_rubocop_xml.rb $err`
echo $mess
```

```cmd
% sh run_diffcop.sh
- test_rubocop.rb:1: Missing top-level class documentation comment.
- test_rubocop.rb:1: Missing magic comment `# frozen_string_literal: true`.
- test_rubocop.rb:3: Prefer single-quoted strings when you don&#39;t need string interpolation or special symbols.
```

## 2. rubocopエラーはGithubAPI経由でGithubで投稿する

```sh:run_diffcop.sh
err=$( git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err

mess=`bundle exec ruby parse_rubocop_xml.rb $err`
echo $mess

if [ "$mess" ]; then
  POST_BODY="{\"body\": \"RUBOCOP WARNING!!! \n $mess "}"
  curl -XPOST \
    -H "Authorization: token GITHUB_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$POST_BODY" \
    https://api.github.com/repos/:owner/:repo/issues/pr_number/comments
fi

```
ここまではローカルでファイルの差分だけでrubocopエラーがGithubに通知できる

## 3. 上のロジックをDroneCIに持っていく
```yml:.drone.yml
# skip
run_diffcop:
    image: your_image
    secrets: [
      GITHUB_ACCESS_TOKEN,
      DRONE_PULL_REQUEST,
      DRONE_COMMIT_SHA
    ]
    commands:
      - sh run_diffcop.sh
    when:
      event: [pull_request]

# skip
```
droneCIのenvironmentの詳細は[こちら](https://github.com/drone/docs/blob/master/content/usage/reference/environment.md)
droneCIでrubocopを実行すると、２つの変更が必要
1. `git fetch upstream master` は　`git fetch origin master`になる
2. `https://api.github.com/repos/:owner/:repo/issues/pr_number/comments`
は `https://api.github.com/repos/:owner/:repo/issues/$DRONE_PULL_REQUEST/comments`になる

```sh:run_diffcop.sh
err=$( git fetch origin master && git diff -z --name-only FETCH_HEAD.. \
| xargs -0 bundle exec rubocop-select \
| xargs bundle exec rubocop \
--require rubocop/formatter/checkstyle_formatter \
--format RuboCop::Formatter::CheckstyleFormatter \
| bundle exec checkstyle_filter-git diff FETCH_HEAD)
echo $err

mess=`bundle exec ruby parse_rubocop_xml.rb $err`
echo $mess

if [ "$mess" ]; then
  POST_BODY="{\"body\": \"RUBOCOP WARNING!!! \n $mess "}"
  curl -XPOST \
    -H "Authorization: token GITHUB_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$POST_BODY" \
    https://api.github.com/repos/:owner/:repo/issues/pr_number/comments
fi

```

以上！
これからプルリクを更新するたびに、rubocopを実行してもらう
<img width="768" alt="スクリーンショット 2018-12-14 0.13.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/121276/2fd3a80a-cf29-c6cc-413b-c621e57c63e8.png">


## おまけ
### プルリクでrubocopエラーが起こるファイルの行ごとでエラー内容を投稿する (comment inline)

```ruby:diffcop.rb

require 'nokogiri'
require "cgi"
require 'pry'
require 'httparty'

@drone_link = ARGV[0]
@commit_id = ARGV[1]
@pull_number = ARGV[2]
@github_token = ARGV[3]
@hunk_headers_data = {}

puts @commit_id
puts @github_token
puts @drone_link

def run_diffcop
  analyze_commit_diff_hunks
  mess = pretty_rubocop_error
  push_to_github mess
end

def analyze_commit_diff_hunks
  diff = fetch_commit_diff
  @hunk_headers_data = parse_diff diff
end

def fetch_commit_diff
  HTTParty.get("https://api.github.com/repos/orcainc/homeup/commits/#{@commit_id}",
    headers: {
      "Authorization": "token #{@github_token}",
      "Content-Type": "application/json",
      "Accept": "application/vnd.github.v3.diff",
      "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
    }
  )
end

def parse_diff diff
  res = {}
  key = nil
  diff.each_line do |str|
    new_key = str.scan(/diff --git a\/(.*)b\//).flatten[0]
    if new_key
      key = new_key.strip
      res[key] = {}
    end
    hunk_header = str.scan(/@@ \-\d+,\d+ \+(\d+),(\d+) @@/).flatten.map(&:to_i)
    if hunk_header.length > 0
      res[key]["hunks"] ||= []
      res[key]["all_hunks_pos"] ||= []
      res[key]["hunks"] << {position: hunk_header[0], range: hunk_header[1]}
      res[key]["all_hunks_pos"] << hunk_header[0]
    end
  end
  res
end

def pretty_rubocop_error
  xml_doc = Nokogiri::XML(rubocop_errors)
  error_messages = []
  xml_doc.xpath("//file").each do |elm|
    elm.children.search("error").each do |error|
      err_line = error.attributes["line"].value
      file_name = elm.attributes["name"].value
      error_messages <<  {path: file_name,
                          position: get_position_of_comment(file_name, Integer(err_line)),
                          body: error.attributes["message"].value
                          }
    end
  end
  error_messages
end

def rubocop_errors
  data_message_errors = `git fetch upstream master && git diff -z --name-only FETCH_HEAD.. \
  | xargs -0 bundle exec rubocop-select \
  | xargs bundle exec rubocop --force-exclusion --config .rubocop_v2.yml \
   --require rubocop/formatter/checkstyle_formatter \
   --format RuboCop::Formatter::CheckstyleFormatter \
  | bundle exec checkstyle_filter-git diff FETCH_HEAD`
  data_message_errors
end

def get_position_of_comment file_name, line
  comment_pos = 0
  all_pos = @hunk_headers_data[file_name]["all_hunks_pos"].dup
  chain_pos = all_pos.keep_if {|v| v < line}
  if chain_pos.length > 1
    pivot = chain_pos.pop
    chain_pos.each do |po|
      comment_pos += get_range_from_hunk file_name, po
      comment_pos += 1 # +1 for hunk header line
    end
    comment_pos += (line - pivot + 1)
  else
    comment_pos = line - all_pos[0] + 1
  end
  comment_pos
end

def get_range_from_hunk key, pos
  @hunk_headers_data[key]["hunks"].detect {|hunk| hunk[:position] == pos}[:range]
end

def push_to_github mess
  response = HTTParty.post("https://api.github.com/repos/orcainc/homeup/pulls/#{@pull_number}/reviews",
    headers: {
      "Authorization": "token #{@github_token}",
      "Content-Type": "application/json",
      "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
    },
    body: {
      "body": "RUBOCOP WARNING!!! \n #{@drone_link}",
      "event": "COMMENT",
      "comments": mess,
    }.to_json
  )
  puts response
end

run_diffcop

```

```yml:.drone.yml
run_diffcop:
    image: your_image
    secrets: [
      GITHUB_ACCESS_TOKEN,
      DRONE_PULL_REQUEST,
      DRONE_COMMIT_SHA
    ]
    commands:
      - ruby diffcop.rb $DRONE_BUILD_LINK $DRONE_COMMIT_SHA $DRONE_PULL_REQUEST $GITHUB_ACCESS_TOKEN
    when:
      event: [pull_request]
```
結果
<img width="818" alt="スクリーンショット 2018-12-14 0.28.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/121276/7a2714f1-3c02-dadc-5f4d-823adde59575.png">