Help us understand the problem. What is going on with this article?

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

概要

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

その際に作成した「指定された期間にマージされた」PR一覧を出力するNodeスクリプトを紹介します。

request-promise というHTTPクライアントのライブラリと、日付のフォーマットにmoment.jsという時刻操作のライブラリを使用しています。

準備

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

プロジェクトを作成して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を実行させるように記述を変更すると、こんな感じになります。

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

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トークンを指定します。

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

ソース(全量)

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

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のアクセストークンの発行方法
- request-promiseの簡単な使い方(urlとfetchOptions)
- PRを取得するためのURL https://api.github.com/repos/${owner}/${repo}/pulls?state=closed

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away