最近githubがhubの代わりにghという新しいgithub用のCLIツールを出していました。cli/cliという位置にあるので組織名とリポジトリ名がすごい。特等席。
例えばmacでは以下の様な形でインストールすると、このghコマンドが使える様になる。
$ brew install github/gh/gh # upgrade # brew update && brew upgrade gh
現在のcwdが所属していそうなgithub repositoryに対してその場でPRの作成ができたり、ステータスが見れたりとけっこう便利。
まぁそれは置いておいて、このghコマンドを使っていたときに、どうも自動で更新チェックをやってくれるようだった。これをどのタイミングでやっているのかなーと気になったのでソースコードを覗いて見たらなるほど~と思うことがあったのでメモをしておく。
ghコマンドの更新チェックのメッセージ
ghコマンドにアップデートがあったときにはどのようなメッセージが表示されるかというと以下の様な表示がされる。
$ gh pr status ... A new release of gh is available: 0.5.4 → v0.5.7 https://github.com/cli/cli/releases/tag/v0.5.7
まぁそういう感じで最新のバージョンを使うことを促される。 この更新チェックを行っている処理があるはず。
更新チェックを行っているコード
そんなわけでmain.goを覗いてみるとほとんどこのファイルそのものが答え。
なるほど、立ち上がると同時に徐にgoroutineを動かしていた。なるほど。これは賢い。
こういう感じのコードになっている(コメントは勝手に追加したもの)。
func main() { currentVersion := command.Version // 更新確認のためのgoroutineを起動 updateMessageChan := make(chan *update.ReleaseInfo) go func() { rel, _ := checkForUpdate(currentVersion) updateMessageChan <- rel }() // 通常の処理 hasDebug := os.Getenv("DEBUG") != "" if cmd, err := command.RootCmd.ExecuteC(); err != nil { printError(os.Stderr, err, cmd, hasDebug) os.Exit(1) } // 起動したgoroutineを待つ newRelease := <-updateMessageChan if newRelease != nil { // 更新があったときのメッセージ msg := fmt.Sprintf("%s %s → %s\n%s", ansi.Color("A new release of gh is available:", "yellow"), ansi.Color(currentVersion, "cyan"), ansi.Color(newRelease.Version, "cyan"), ansi.Color(newRelease.URL, "yellow")) stderr := utils.NewColorable(os.Stderr) fmt.Fprintf(stderr, "\n\n%s\n\n", msg) } }
なるほど。コマンドを立ち上げたタイミングで安直にgoroutineを立ち上げてしまえば良い。便利。
あとはふつうに通常の処理を書いてしまって、終了したタイミングでgoroutineを待てば良い。ふつう実行自体が終わったタイミングでは終了しているだろうし、githubに対するコマンドなどというのはネットワークがつながっていなければ何もできないのだから、常にrequestしてしまっても良い。
これは何か他にCLIのツールを作るときには参考になるかもしれないなーと思ったりした。
ちなみに更新チェックの処理自体の実装は
ちなみにcheckforUpdate()自体は以下の様な実装になっていて、pipeでつなげていたりしたときには省略されそうな感じ。
func shouldCheckForUpdate() bool { return updaterEnabled != "" && utils.IsTerminal(os.Stderr) } func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) { if !shouldCheckForUpdate() { return nil, nil } client, err := command.BasicClient() if err != nil { return nil, err } repo := updaterEnabled stateFilePath := path.Join(context.ConfigDir(), "state.yml") return update.CheckForUpdate(client, stateFilePath, repo, currentVersion) }
処理自体も特に複雑なことをしているわけではなくgithubのreleases apiを呼んで良い感じにやっていっているだけの模様。
$ http -b https://api.github.com/repos/cli/cli/releases
[
{
"assets": [
{
"browser_download_url": "https://github.com/cli/cli/releases/download/v0.5.7/gh_0.5.7_checksums.txt",
"content_type": "text/plain; charset=utf-8",
"created_at": "2020-02-20T22:23:34Z",
"download_count": 16,
"id": 18185843,
"label": "",
"name": "gh_0.5.7_checksums.txt",
"node_id": "MDEyOlJlbGVhc2VBc3NldDE4MTg1ODQz",
"size": 1100,
"state": "uploaded",
"updated_at": "2020-02-20T22:23:35Z",
"uploader": {
"avatar_url": "https://avatars2.githubusercontent.com/u/887?v=4",
"events_url": "https://api.github.com/users/mislav/events{/privacy}",
"followers_url": "https://api.github.com/users/mislav/followers",
"following_url": "https://api.github.com/users/mislav/following{/other_user}",
"gists_url": "https://api.github.com/users/mislav/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/mislav",
"id": 887,
"login": "mislav",
"node_id": "MDQ6VXNlcjg4Nw==",
"organizations_url": "https://api.github.com/users/mislav/orgs",
"received_events_url": "https://api.github.com/users/mislav/received_events",
"repos_url": "https://api.github.com/users/mislav/repos",
"site_admin": true,
"starred_url": "https://api.github.com/users/mislav/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mislav/subscriptions",
"type": "User",
"url": "https://api.github.com/users/mislav"
},
"url": "https://api.github.com/repos/cli/cli/releases/assets/18185843"
},
...
]