はじめに
GitLab API は以前から知っていましたが、これまで業務で利用する機会がなかなかありませんでした。この記事をご覧の方はご存知だと思いますが、GitLab API は REST API として非常に充実しており、GitLabの画面上で操作することができる API を実行することができます。使い方によってはチーム開発における様々な指標にすることができるため、この記事では、利用方法の1つとして GitLab API の Merge Request のコメントを集計する方法を紹介したいと思います。
背景ときっかけ
私の所属するチームは成長中の若手のメンバーが多いため、コードレビューに一定のコストを費やしています。また、レビュアー育成も現在進行中であるため、私自身チームメンバーのレビューに携わることが多いです。メンバーのレビューを続けているとメンバーの弱点が見えて来るので、GitLab API を利用したコメント集計ツールを作成し、育成観点からコメントを累計化した指標を作成することにしました。
Personal Access Token の作成
GitLab API の利用には、personal access token
の作成が必要になります。
Setting > Edit profile > Access Tokens から Token 作成画面を開き、
- Name
- Scopes > api にチェック
の設定し「Create personal access token」で token を作成します。
Project の Merge Request 情報を取得する
Merge Request のコメントを集計するには、対象となる Project ID の取得が必要になります。まずは Project ID をもとに Merge requests API を利用し、Merge Request の情報を取得します。ドキュメントに記載されていますが、Project の Merge Request の一覧取得は、以下のように行います。
1. data class の準備
ドキュメントに記載されている必要な項目を設定します。他にも必要な項目があれば追加してください。
このとき、Merge Request の配列がレスポンスとなるため、parser も用意しておきます。不要な情報は ignoreUnknownKeys = true
を設定して無視します。
既取得したい Merge Request の iid が分かっている場合は、次の Note の取得を行います。
@Serializable
data class MergeRequest (
val iid: Int,
@SerialName("project_id")
val prjId: Int,
val state: String,
@SerialName("merged_at")
val mergedAt: String?,
val assignee: User?,
val reviewers: List<User?>,
@SerialName("web_url")
val webUrl: String,
) {
companion object {
fun parse(jsonString: String): List<MergeRequest> {
val formatter = Json { ignoreUnknownKeys = true }
return formatter.decodeFromString(ListSerializer(serializer()), jsonString)
}
}
}
@Serializable
data class User (
val id: Int,
val name: String,
val username: String,
)
assignee, reviewer が設定されていない場合は、null となるため、optional 型で Serialize できるようにします。
2. Merge Requests を取得する
HttpClient は fuel を使用していますが、導入などの詳しい説明はこちらをご覧ください。
fun main() {
val header: HashMap<String, String> = hashMapOf("PRIVATE-TOKEN" to "MY_TOKEN")
val httpAsync = "https://gitlab.com/api/v4/projects/:id/merge_requests"
.httpGet()
.header(header)
.responseString { request, response, result ->
when (result) {
is Result.Failure -> {
println(result.getException())
}
is Result.Success -> {
val mergeRequests = MergeRequest.parse(result.get())
}
}
}
httpAsync.join()
}
このリクエストの結果として、Project に紐ずく Merge Request の情報を取得できます。
(以後、リクエスト部分は一部省略します)
この時、長く運用している Project になると Merge Request の数も多くなるため、必要以上の情報になることがあります。GitLab API では、Search API が用意さており、クエリパラメータとして URL に設定することで、一定の検索条件を付けることができます。
例えば、Merge Request の状態を条件や更新日を条件とする場合は、以下のような URL でリクエストします。
val httpAsync = "https://gitlab.com/api/v4/projects/:id/merge_requests?state=all&updated_after=2021-12-01T08:00:00Z"
このように Search API を利用すれば、必要な Merge Request の情報を取得することができます。
Merge Request Note を取得する
Merge Request の情報が取得できたので、次は Merge Request のコメントを取得してみましょう。
GitLab API では、Note API とうコメント集計の API が用意されています。さらに見ていくと Merge Request > List all merge request notesという API が用意されており、この API を利用すれば、コメント情報を取得できそうです。Merge Request API と同様に、Note の配列がレスポンスとなるため、parser も用意しておきます。
@Serializable
data class Note (
val id: Int,
val body: String
)
では、実際にリクエストしてみることにしましょう。
val httpAsync = "https://gitlab.com/api/v4/projects/:id/merge_requests/:merge_request_iid/notes"
.httpGet()
.responseString { request, response, result ->
when (result) {
is Result.Success -> {
val notes = Note.parse(result.get())
}
}
}
httpAsync.join()
実際にリクエストしてみると分かるのですが、この Note API はドキュメントに all notes
と記載されているよう、Overview や GitLab 上付与されるラベル変更時のメッセージといった一切合切の情報が note として入って来てしまいます。body
にはこのようなノイズとなる note も含まれてしまうので、運用時は prefix を付けて note をフィルタリングして活用しています。
一方で、この Note API には一つの欠点がありました。それは、後述のように Discussion では最後のコメントしか取れない
ということです。一番欲しい Note としては、一番初めにコメント(問題提起)している Note なのですが、これがレスポンスに含まれていませんでした。
この Note API のイマイチ使い所が分からないですね。。
Discussions を取得する
GitLab API をもう少し見てみると、Discussions API という API が用意されていることに気付きました。この Discussion Merge request API により a list of all discussion items
としてコメントを取得できるようです。ドキュメントのレスポンス形式を確認すると、上記の Note がリストで返却される interface になっていることが分かります。コメントを取得するため、同様の data class を定義します。
@Serializable
data class Discussion (
val id: String,
val notes: List<Note>
)
既に Note の data class を定義しているので、シンプルに定義することできますね。
val httpAsync = "https://gitlab.com/api/v4/projects/:id/merge_requests/:merge_request_iid/discussions"
.httpGet()
.responseString { request, response, result ->
when (result) {
is Result.Success -> {
val Discussions = Discussion.parse(result.get())
}
}
}
httpAsync.join()
このように Note API と Discussion API を利用することで Merge Request のコメント一覧を取得することできます。
重複を排除する
最終的に実現したいことは、これらの一覧にして CSV として出力することなので、Note と Discussion の Note を merge してリストにして行きます。出力時に必要な Merge Request の情報もあったので、Merge Request の情報も合わせて出力項目を作成します。私の場合、主に以下の情報を出力していますが、prefix
はコメントに対する独自ルール温度感を prefix で添えて抽出した結果になります。
class NoteInfo(mergeRequest: MergeRequest, note: Note) {
val iid: Int = mergeRequest.iid
val noteId: Int = note.id
val prefix: String?
val assignee: String = mergeRequest.assignee.name
val reviewer: String? = mergeRequest.reviewers.lastOrNull()?.name
val state: String = mergeRequest.state
val mergedAt: String? = mergeRequest.mergedAt
val webUrl: String = mergeRequest.webUrl
val body: String = note.body
init {
val regex = "([a-zA-Z]{3,6})(([a-zA-Z]{3,10})):"
val matcher = Pattern.compile(regex).matcher(body)
if (matcher.find()) {
prefix = matcher.group(1) + "/" + matcher.group(2))
}
}
}
既にお気付きかも知れませんが、Note と Discussion の Note では重複が発生します。具体的には、Discussion の最終コメントの Note が重複してきます。コメントの内容だけ参考にする分にはあまり問題になりませんが、KPI としてコメント数を管理したいような場合、正しい結果を得ることができません。
CSV で結果を出力する
最後は、merge した NoteInfo を CSV に出力します。
fun export(list: List<NoteInfo>) {
FileWriter("result.csv", Charset.forName("SJIS")).use {
createHeader(it)
for (i in list.indices) {
val rec = list[i]
it.write(rec.iid)
it.write(",")
it.write(rec.body)
it.appendLine()
}
it.close()
}
}
private fun createHeader(f: FileWriter) {
f.write("Id")
f.write(",")
f.write("Comment")
f.appendLine()
}
(出力項目は一部省略しています。)
まとめ
以上のように GitLab API の
- Merge request API
- Note API > Merge requests
- Discussion API > Merge requests
を利用することで、Merge Request のコメントを一括取得することができます。
API として一回のリクエストで完結できない点は、改善して頂けることを望みますが、Merge Request でのコメントはプロダクトの品質担保やメンバーの育成などの観点からも有効に活用できると考えております。
このように GitLab API を利用すれば、チームにとって必要な情報を取得することが可能なので、一度試してみてはいかがでしょうか。