GithubのPullRequest一覧をNodeで取得する

# 概要
Githubを使って開発をしていますが、チームでソースコードを共有するための文化として、前日マージされたPRを全て読むということを行っています。

その際に作成した「指定された期間にマージされた」PR一覧を出力するNodeスクリプトを紹介します。  
`request-promise` というHTTPクライアントのライブラリと、日付のフォーマットに`moment.js`という時刻操作のライブラリを使用しています。

- **request-promise** ... https://github.com/request/request-promise
- **moment.js** ... https://momentjs.com/


# 準備
プロジェクト作成と、必要なライブラリのインストールです。（以前の記事でも同じことを紹介していますが毎回やることなので書いておきます）

## プロジェクトを作成してES2015を使えるようにする
```
mkdir list-pull-requests
cd list-pull-requests
npm init
(entry pointはlistprs.js、LicenseはMIT)

npm i -S babel-cli babel-preset-env
```

## 必要ライブラリのインストール
```
npm i -S request request-promise moment
```

package.jsonでscriptsにbabel-nodeを実行させるように記述を変更すると、こんな感じになります。

```json:package.json
{
  "name": "list-pull-requests",
  "version": "1.0.0",
  "description": "",
  "main": "listprs.js",
  "scripts": {
    "start": "babel-node listprs.js --presets env"
  },
  "author": "tashxii",
  "license": "MIT",
  "dependencies": {
    "babel-cli": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "moment": "^2.24.0",
    "request": "^2.88.0",
    "request-promise": "^4.2.4"
  }
}
```

# 実装内容
Github APIを呼び出すには、API Access Tokenが必要になります。作成して内容をコピーしておいてください。忘れても再生成はできます。

## API Access Tokenの発行
文章でも書いておくと
- アカウントアイコンを右クリック、[Settings]->[Developer settings]->[Personal access token]の順にクリック
- もしくは、いきなり https://github.com/settings/tokens にアクセス
- [Generate new access token]をクリック
- チェック内容は、[repo]をすべてチェックしておいてください。
- [Generate token]ボタンで生成できます。

以下のヘルプの画像に従うとわかりやすいです。
- https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line

## request-promiseの使い方とGithub APIを呼び出し 
`request-promise`は、**rp**の関数にurlと、後述のfetchOptionsを渡して*then*,*catch*を呼び出すだけよいです。

Github APIのURLもシンプルです。

```
GET /repos/:owner/:repo/pulls
```

上記APIで、以下のガイドにある`state=closed`を指定するだけでOKです。
- https://developer.github.com/v3/pulls/#list-pull-requests


```javascript
import rp from "request-promise"
import moment from "moment"

...(一部省略)

const listPullRequests = (token, owner, repo, sinceTime, untilTime) => {
  const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed`
  rp(url, fetchOptions(token))
    .then((response) => {
      return JSON.parse(response)
    })
    .then((prs) => {
      console.log(" # " + repo)
      prs.forEach((pr) => {
        if (pr.merged_at) {
          const mergedTime = Date.parse(pr.merged_at)
          // 該当期間内にマージされていれば出力
          if (sinceTime <= mergedTime && mergedTime < untilTime) {
            console.log("   - " + pr.title + " (close:" + moment(new Date(pr.closed_at)).format("YYYY/MM/DD hh:mm)"))
            console.log("     - " + pr.html_url)
          }
        }
      })
      console.log("")
    })
    .catch((error) => {
      console.log(error)
    })
}
```

fetchOptionsは、以下の通りです。`Authorization`に生成したAPIトークンを指定します。

```javascript
const fetchOptions = (token) => {
  return {
    method: "GET",
    headers: {
      "User-Agent": "request",
      Authorization: `token ${token}`
    },
    isJson: true,
  }
}
```

## ソース（全量）
引数チェックと使用方法が行をしめていますが、基本的には上で説明した関数を呼び出すmain関数が追加されているだけです。

```javascript:list-pull-requests.js
import rp from "request-promise"
import moment from "moment"

const main = (token, owner, repos, since, until) => {
  const sinceTime = Date.parse(since)
  const untilTime = (until) ? Date.parse(until) : Date.now()
  repos.forEach(repo => {
    listPullRequests(token, owner, repo, sinceTime, untilTime)
  })
}

const listPullRequests = (token, owner, repo, sinceTime, untilTime) => {
  const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed`
  rp(url, fetchOptions(token))
    .then((response) => {
      return JSON.parse(response)
    })
    .then((prs) => {
      console.log(" # " + repo)
      prs.forEach((pr) => {
        if (pr.merged_at) {
          const mergedTime = Date.parse(pr.merged_at)
          if (sinceTime <= mergedTime && mergedTime < untilTime) {
            console.log("   - " + pr.title + " (close:" + moment(new Date(pr.closed_at)).format("YYYY/MM/DD hh:mm)"))
            console.log("     - " + pr.html_url)
          }
        }
      })
      console.log("")
    })
    .catch((error) => {
      console.log(error)
    })
}

const fetchOptions = (token) => {
  return {
    method: "GET",
    headers: {
      "User-Agent": "request",
      Authorization: `token ${token}`
    },
    isJson: true,
  }
}

const usage = () => {
  console.log("List up the PRs which are merged in the specified period.")
  console.log("Usage: npm start <api_access_token> <owner> <repositories> <since> <until>")
  console.log("  <access_token>")
  console.log("    Access token for github api, [repo] must be checked.")
  console.log("    To generate token, access to 'https://github.com/settings/tokens' and click [generate new token]")
  console.log("  <owner>")
  console.log("    Specify the owner of github repositories.")
  console.log("  <repositories>")
  console.log("    Specify the github repositories separated by comma. ex) aaa,bbb")
  console.log("  <since>")
  console.log("    Specify the 'since' time, don't forget to add timezone.")
  console.log("    The PRs merged at 'since' time contain the result.")
  console.log("    ex) \"2019-01-01 00:00:00 +9:00\" (JST case)")
  console.log("  <until>")
  console.log("    An optional parameter.")
  console.log("    If omitted, the PPs merged after 'since' time (no until condition) will be listed-up.")
  console.log("    Specify the 'until' time, don't forget to add timezone.")
  console.log("    The PRs merged at 'until' time *DO NOT* contain the result.")
  console.log("    ex) \"2019-01-31 00:00:00 +9:00\" (JST case)")
}

const parseArgs = (argv) => {
  if(argv.length < 8) return undefined
  if(isNaN(Date.parse(argv[7]))) {
    console.error(`<since> time "${argv[7]}" is not date format. Specify correct format. Ex) "2019-01-01 00:00:00 +9:00"`)
    return undefined
  }
  if(argv[8] && isNaN(Date.parse(argv[8]))) {
    console.error(`<until> time "${argv[8]}" is not date format. Specify correct format. Ex) "2019-01-02 00:00:00 +9:00"`)
    return undefined
  }
  return {
    token : argv[4],
    owner: argv[5],
    repos: argv[6].split(","),
    since: argv[7],
    until: argv[8],
  }
}

const config = parseArgs(process.argv)
if (config) {
  main(config.token, config.owner, config.repos, config.since, config.until)
} else {
  usage()
}
```

## 使い方
コマンドラインから、npm startに以下の引数をつけて実行します。
標準出力に、リポジトリごとのPRタイトルとURLがMD形式で出力されます。

```
npm start <access token> <owner> <repos> <since> [<until>]
```
- access token ... 生成したアクセストークン
- owner ... Githubのオーナー名
- repos ... 取得したいリポジトリをカンマ区切りで記述
- since ... 取得開始日（＞＝で判定） タイムゾーンも付けます。例："2019-01-01 00:00:00 +9:00"
- until ... 取得終了日（＜で判定） 省略すると現在時刻までのPRを取得します。



#まとめ

以下のことがわかると思います。
- [Github APIのアクセストークンの発行方法](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line)
- request-promiseの簡単な使い方（urlとfetchOptions）
- PRを取得するためのURL  `https://api.github.com/repos/${owner}/${repo}/pulls?state=closed`

人のPRを読む文化を導入した結果、ソースコードの共有意識、レビューアー以外の目によるチェック、良いコード、悪いコードなどの気づきが見つかるなど、良いことづくめでした。