リポジトリ名から“author/”を含んだ名前を確認するCLIを作った

## tl;dr
https://github.com/takagiy/repo_cmd

```console
$ repo repo_cmd
takagiy/repo_cmd
```

```console
$ git clone $(repo url repo_cmd)
Cloning into 'repo_cmd'...
```

## 動機
昨今のパッケージマネージャでは、パッケージのソースとしてGitHub等のリポジトリを指定するようなものがよくみられるようになってきた。例えば、自分が使っているVimのプラグインマネージャ[\[`Volt`\]](https://github.com/vim-volt/volt)もそうで、以下のようにしてプラグインを追加する。

```
$ volt get junegunn/fzf.vim
```

あとは`elm install`とか、そして`git clone`とかもそういう感じだ。

こういうツールを使うときはリポジトリを同定するために少なくとも`author/name`という形式の指定が必要だが、インストールしたいリポジトリは思い浮かんでも、authorまではいちいち覚えていないことが多い。そのため、なにかをインストールしようとする度にブラウザを開いてauthorを確認するということがよく起きていた。

これは煩わしい。リポジトリ名に対応する`author/name`という文字列をコマンドラインから取得できるツールがあると便利そうだ。そういうことをしばらく考えていて何度か調べたがその限りでは見当たらなかった(GitHub関連のCLIとしては`hub`がメジャーっぽいけど[そういう機能は現状ないらしい](https://github.com/github/hub/issues/833))。そういうわけで自分で書いた。

## できたもの
与えられたリポジトリ名を[GitHub REST API v3/Search](https://developer.github.com/v3/search/)に問い合わせて、一致したリポジトリの情報を表示する。出力は以下のようにシェルのcommand substitution等から利用されることを想定して最小に留めた。

```console
$ volt get $(repo fzf.vim) # OR volt get (repo fzf.vim) IN fish shell
```

取得できるフォーマットには種類があって、以下の通り。

* `{author}/{name}`
* 
    ```console
    $ repo repo_cmd # OR repo full-name repo_cmd
    takagiy/repo_cmd
    ```

* `https://"...".git`

    ```console
    $ repo url repo_cmd
    https://github.com/takagiy/repo_cmd.git
    ```

* `git://"...".git`

    ```console
    $ repo git-url repo_cmd
    git://github.com/takagiy/repo_cmd.git
    ```

* `https://"..."`

    ```console
    $ repo link repo_cmd
    https://github.com/takagiy/repo_cmd
    ```

前述のAPIは認証しないと一応のリミットがあるけど、かなり緩くてコマンドを連打しない限り制限がかかることもなかったし、ほんの1分程度解除されるので認証機能は実装していない。[^rate_limit]

[^rate_limit]: 制限についてドキュメントちゃんと読んでなくてうろ覚え

検索の際、リポジトリ名が一致するかどうかはcase insensitiveに判断している。

```console
$ repo satysfi
gfngfn/SATySFi
```

### インストール

`Rust`のパッケージマネージャ[\[`Cargo`\]](https://doc.rust-lang.org/stable/cargo/)でインストールできる。バイナリ配布は現状ない（ライセンス表記難しい…）。

```console
$ cargo install repo_cmd
```

（`repo`というcrateが既にあったため`repo_cmd`というcrate名になった）

### リポジトリ
[GitHub](https://github.com/takagiy/repo_cmd)←これ

## やや詳しいこと

`Rust`で書いた。初心者です。

APIのレスポンスはページと呼ばれるまとまりで返ってくるから、ページの中に与えられた名前と一致するリポジトリがあればそれを返し、なければ次のページをリクエストする。ただし、一定数のページを確認しても見つからなければ検索を諦める。以下のようになる。[^max_page]

[^max_page]: 記事を書いてて思ったけど`1..MAX_PAGE`じゃなくて`1..(MAX_PAGE + 1)`か…？

```rs
let repository = {
    let mut repository = None;
    for page in 1..MAX_PAGE {
        let response: Response = client
            .get(API_URL)
            .header(header::USER_AGENT, user_agent())
            .query(&[
                ("per_page", "10"),
                ("page", &page.to_string()),
                ("q", query),
            ])
            .send()
            .context("Connection failed")?
            .json()
            .context("Invalid API response")?;

        let mut candidates = response.items.ok_or(anyhow!(
            "Invalid API response, '{}'",
            response.message.as_deref().unwrap_or("")
        ))?;

        let found = candidates
            .drain(..)
            .find(|repo| repo.name.to_ascii_lowercase() == normal_query);

        if found.is_some() {
            repository = found;
            break;
        }
    }
    repository
}
.ok_or(anyhow!("No repository named '{}' found", query))?;
```

コマンドライン引数のパースは手書きした。[\[`clap`\]](https://docs.rs/clap/latest/clap/)とかを使ったほうがよかったかもしれないけど~~clapのAPIを読むのが面倒だったので~~`Rust`はほぼ初めてだったので、どういう書き心地なのか気になって地道に書いてみた。

## おわり
最後まで読んでくれてありがとうございました。