にっき

技術的な話題はないです

Rustacean のためのリポジトリ管理ツールを作りました

何度かつぶやいたりしていましたが、READMEを書くのが面倒になったので より多くの人に使ってもらうため少し真面目に解説記事を作りました。

rhq の概要

GitHub - ubnt-intrepid/rhq: Manages your local repositories

言わずと知れた Golang 製のリポジトリ管理ツールである ghq を Rust に移植したものです。 もともと ghqrs というものを作って放置していたのですが、内部の実装に若干気に入らない点があり再度自前で実装したのが本ツールになります。 現状では ghq すべての機能を移植できているわけではないので代替というには若干役不足ですが、自分の欲しい機能は概ね実装できたので思い切って公開することにしました。

基本的に出来ることは ghq と同じような感じですが,あまり使っていない機能などは省略しています。 下のコマンド実行例を見れば雰囲気だけでも伝わると思います。

$ rhq clone ubnt-intrepid/rhq
# git clone https://github.com/ubnt-intrepid/rhq.git ~/.rhq/github.com/ubnt-intrepid/rhq と等価
$ rhq list | sk

インストール方法

現状はソースのみの配布なため、Rust および Cargo が必要になります(ビルド済みバイナリの配布は検討中です)。 すでに Rust の開発環境が整っている場合は cargo コマンドでインストールできます。

$ cargo install rhq
# 開発版を使用する場合
$ cargo install --git https://github.com/ubnt-intrepid/rhq.git

ディレクトリ構成

基本的には ghq (および Golang)と同様、ルートディレクトリ(デフォルトでは ~/.rhq)直下に ホスト名/ユーザ名/リポジトリ名 という形式でクローンしたリポジトリが格納されます. ghq では複数のルートディレクトリを指定することが出来るようになっていますが、クローン時のディレクトリの決定が面倒だったためrhq では原則として単一のルートディレクトリを用いるようにしました. ただしそれでは不便な場合もあるので,後述するようにリポジトリ探索用のディレクトリを追加するための設定項目を設けています.

  • ~/.rhq/
    • github.com/
      • user1/
        • project1/
        • project2/
      • user2/
        • project1/
    • gitlab.com/
      • user1/
        • project3/

コマンド一覧

rhq clone [<query>] [--arg=<arg>] [-n | --dry-run]

<query> に指定した文字列を元にリモートリポジトリのURLを推測し, ルートディレクトリ(デフォルトでは ~/.rhq)下の指定場所にクローンします(要は ghqghq get です)。 例えばこのプロジェクトのリポジトリを取得したい場合は次のようにします.

$ rhq clone ubnt-intrepid/rhq
# git clone https://github.com/ubnt-intrepid/rhq.git ~/.rhq/github.com/ubnt-intrepid/rhq が実行される

--arg オプションで git clone に渡すオプションを指定できます。 ghq get では --shallow オプションなどを用いてクローン時のオプションを設定しますが,より柔軟な制御をおこなえるように rhq では Git のオプションをそのまま渡す方針を取りました。

<query> を省略した場合は,標準入力から1行ずつ読みこみリポジトリのクローンを行います。

rhq list

ルートディレクトリ直下にあるリポジトリのパスを取得し、標準出力に表示します。 後述する探索用ディレクトリに vim-plug や zplug のディレクトリを指定しておくことで,(別途スクリプトを用意することなく)一括してリポジトリを管理することが可能です。

rhq foreach [-n | --dry-run] <command> [<args>...]

管理下のリポジトリディレクトリ下で指定したコマンドを実行します。 例えば、管理下のリポジトリ全てに対しリモート側の変更を同期する場合は次のように実行します。

$ rhq foreach -- git fetch --all

現状はシングルスレッドで実行するためすべてのコマンドの実行が完了するまでに時間を要します。そのうち並列しようと思ってはいますが現状では未定です…

rhq completion <shell> [<out-file>]

各種シェル用の補完スクリプトを生成します。 clap の補完スクリプト生成機能をそのまま使っているだけですが. <out-file> にはスクリプトの出力先のパスを指定し,省略した場合は標準出力にスクリプトを吐き出します.

設定ファイル

TOML 形式での設定ファイルの読み込みをサポートしています. 設定ファイルは ~/.config/rhq/config~/.rhqconfig の順番で読み込み,各項目は上書き・追記されます. 現在は以下の項目を用意しています.

root = "~/.rhq"

supplements = [
  "~/.go/src",
  "~/.vim/plugged",
  "~/.zplug/repos",
  "~/.dotfiles"
]

応用例

ghq 側の応用例がほぼそのまま使うことができます。 そのため、そこまで説明する必要はない気もしますが、はじめての方向けに簡単に紹介しておきます。

GitHub から特定ユーザのリポジトリを一括クローンする

GitHub APIコマンドラインJSONを整形できる jq を使った例です。 現状は rhq clone が並列化をしていないので,愚直に xargs で並列化したほうが良い気がしますが…

$ ghuser=ubnt-intrepid
$ curl -s "https://api.github.com/users/$ghuser/repos?max_pages=100" \
  | jq -r '.[].name' \
  | awk '{printf("'$ghuser'/%s\n",$1)}'
  | rhq clone --arg="--depth 50"

この辺 を使って直接 GitHub API を呼び出しても面白そう

シェル・テキストエディタとの連携

peco や fzf などの fuzzy selector を用いることで端末上での作業が効率化できることが知られています。 例として、Zsh 上で Rust 製の fuzzy selector である skim と連携した例を紹介します. 下のコードを ~/.zshrc などに書いておくことで, Ctrl+Gリポジトリの一覧を skim で表示し, 選択したディレクトリに移動することが出来ます.

function __fuzzy-select-repositories() {
  local selected=$(rhq list | sk --prompt='REPOS> ' --query="$LBUFFER")
  if [[ -n $selected ]]; then
    BUFFER="cd \"${selected}\""
    zle accept-line
  fi
  zle clear-screen
}

zle -N __fuzzy-select-repositories
bindkey '^g' __fuzzy-select-repositories

Vim では Unite や ctrlp を使用することが考えられます. 例えば mattn/ctrlp-ghq を使用する場合は次のような感じになります.

Plug 'mattn/ctrlp-ghq'

let g:ctrlp_ghq_command = 'rhq'
let g:ctrlp_ghq_actions = [
  \   { "label": "Open", "action": "Explore", "path": 0 },
  \ ]

" <leader>g でセレクタを起動する
noremap <Leader>g :<C-u>CtrlPGhq<CR>

Visual Studio Code への組み込みですが,せっかくなので自前で拡張機能作ってみましたVisual Studio Code Marketplace で公開しているので ext install vscode-rhq などしてインストールして下さい.

おわりに

今後の計画として、せっかくなので少しリッチな機能を用意しようかなぁと思っています…

  • rhq clone および rhq foreach の並列化
  • custom hook
  • ホストの設定

特に並列化などは良さげな並列処理のライブラリを試してみる意味でも早急に実装しようと考えています…

プルリクエストやツッコミなど、随時募集しています。