この記事は「Go Advent Calendar 2022 3」の12日の記事です!
Goのカレンダー | Advent Calendar 2022 - Qiita
背景・目的
自分は個人のタスク管理にGitHub Projects(ProjectV2)を使っていて、プライベート、個人で開発しているリポジトリのIssueやPullRequestをひもづけてタスク管理的な意味合いで使っている
そして、各リポジトリではだいたい次のような操作を行っている
- 開発
- PR作成
- Web面でPR表示
- Projectへひもづけ
- この箇
5. 各種カスタムフィールドの値を設定 6. PullRequestの中身を確認してMerge
だいたいPullRequestを作るときはコマンドラインからgh
コマンドで作ることが多いので、ついでに上記のProjectへのひもづけ作業もCLIから行えたら楽できる
ただ、GitHubのCLI(コマンドライン)からProjectV2のプロジェクトへIssueやPullRequestを追加できる機能は現時点ではなさそうだった(2022-12-11)
ということで、GitHub CLIの拡張として自分で作ってみた
動作イメージ
GitHub CLIの対話的インターフェースで操作できるようにしている
- ひもづけるプロジェクト
- ひもづける対象
- Issue
- PullRequest
- 今のブランチで出しているPullRequest(これが個人的には一番欲しかったので入れた!)
- Status、Iteration、などの各種フィールドの値
という順番にそれぞれ選択もしくは入力する
インストール
gh extension install swfz/gh-ap
これはGitHub CLI Extensionだからって話だが上記コマンドだけで使えるようになる
権限のスコープ
拡張の機能上project
への権限が必要となる
次のコマンドでproject
へのスコープを追加する
gh auth login --scopes 'project'
操作方法
gh ap
で今いるディレクトリのリポジトリのIssueやPullRequestを扱う
IssueやPullRequestの番号をpecoなど慣れている方法で入れたいパターンもあるかなということで
コマンドラインオプションからでも番号指定できるようにした
それぞれ次のようなオプションで指定可能
- Issue
gh ap -issue ${issueNumber}
- PullRequest
gh ap -pr ${pullRequestNumber}
その他の項目は対話的に決定していく
選択、入力のスキップ
カスタムフィールドの内容をまだ決めたくない場合などもあるかもしれない
その際に選択しなくても良いよう「Skip This Question.」という選択肢を用意してある
テキストの場合は空文字入力でスキップ可能
開発時の話
GitHub CLI 拡張機能の作成 - GitHub Docs
https://docs.github.com/ja/github-cli/github-cli/creating-github-cli-extensionsdocs.github.com
拡張の作成方法に関しては公式の参考にして作業していった
数コマンドですぐ開発に入れたのでとてもやりやすかった
言語は勉強したいなーと思っていたGo言語を選択した
結構時間がかかったがこれのおかげでちょっとは書けるようになった気がする
GitHub APIへのリクエストやghコマンドの実行
CLIで生成したテンプレートのコードにすでに記述されているがgo-gh
というライブラリを使いAPIへのリクエストやCLIのコマンド実行を行っている
cli/go-gh: A Go module for interacting with gh and the GitHub API from the command line.
コードを書くうえで認証情報など設定をせずにREST, GraphQLのクライアントの生成などを行ってくれるので便利(GitHub CLIで使っている認証情報を使っているよう)
また、CLIのコマンドの実行もできるようにgh.Exec
という関数が用意されている
- GitHub CLIで現在のブランチから生成されているPullRequestのデータを取得する関数の例
type Content struct { Id string `json:"id"` Number int `json:"number"` Title string `json:"title"` } func ghCurrentPullRequest() Content { args := []string{"pr", "view", "--json", "id,number,title"} stdOut, _, err := gh.Exec(args...) if err != nil { log.Fatal(err) } var currentPR Content if err := json.Unmarshal(stdOut.Bytes(), ¤tPR); err != nil { panic(err) } return currentPR }
pr view
でブランチから生成されているPullRequestを取得できるのでそれを--json
でJSONの出力にしてGoの構造体に変換している
対話的インターフェースでの操作
GitHub CLIが内部的にsurvey
を使っていたのでそれを使うことにした
ソースコード読んでなるほどこうやるのかっていうのを学びながら進めた
- Issue,PullRequestのリスト(
Content[]
)を渡しsurvey
で選択された選択肢の番号を返す関数の例
type Content struct { Id string `json:"id"` Number int `json:"number"` Title string `json:"title"` } // contentTypeは`Issue` or `PullRequest` func askContentNumber(contentType string, contents []Content) string { var numbers = make([]string, len(contents)) for i, c := range contents { numbers[i] = strconv.Itoa(c.Number) } name := contentType + " Number" qs := []*survey.Question{ { Name: "number", Prompt: &survey.Select{ Message: name, Options: numbers, Description: func(value string, index int) string { return contents[index].Title }, Filter: func(filterValue string, optValue string, optIndex int) bool { return strings.Contains(contents[optIndex].Title, filterValue) }, PageSize: 50, }, }, } answers := map[string]interface{}{} err := survey.Ask(qs, &answers) if err != nil { log.Fatal(err.Error()) } optionAnswer := answers["number"].(survey.OptionAnswer) return optionAnswer.Value }
選択肢の値と、その説明を表示でき、さらに説明に対してインタラクティブにフィルタリングを掛けられる
選択したいのは番号だがIssueやPullRequestのタイトルも表示したい+フィルタリングしたいという使い方もオプションだけで可能だったりして便利だった
おわり
とりあえず動かせそうなところまで作った状態なのでまだ色々改善できる部分はありそうなので引き続き改善していきつつ使っていきたいと思っています
良ければ使ってみてください!