GitHub ラベルをテンプレート化する:gh CLI で一括作成・再利用

GitHub ラベルをテンプレート化する:gh CLI で一括作成・再利用

# デフォルトラベルを全削除
gh label list --json name | jq -r '.[].name' | while IFS= read -r label; do
  gh label delete "$label" --yes
done

# カスタムラベルを一括作成
bash labels.sh

GitHub はリポジトリ作成時に bugenhancement などのデフォルトラベルを 9 種類用意しています。しかし実際のチーム開発では命名規則がバラバラになりやすく、使われないラベルが残りがちです。この記事では gh CLI を使ってデフォルトラベルを削除し、チーム独自のラベルセットをシェルスクリプトとして管理・再利用する方法を解説します。


GitHub のデフォルトラベル

新規リポジトリには以下の 9 種類が自動で作成されます。

ラベル名 用途
bug バグ報告
documentation ドキュメント
duplicate 重複
enhancement 機能改善
good first issue 初心者向け
help wanted 協力者募集
invalid 無効
question 質問
wontfix 対応しない

これらは汎用的すぎて「どれを付けるか」が曖昧になりやすいです。チームの運用に合った命名規則に統一したい場合は、一度削除してカスタムセットに差し替えるのが確実です。


デフォルトラベルを一括削除する

gh label list --json name | jq -r '.[].name' | while IFS= read -r label; do
  gh label delete "$label" --yes
done

gh label list --json name で全ラベル名を JSON 取得し、jq でラベル名だけ抜き出して gh label delete に渡します。--yes は確認プロンプトをスキップします。xargs ではなく while read を使うことで、スペースを含むラベル名でも安全に処理できます。


カスタムラベルのセット設計

プレフィックスでカテゴリを明示する設計が管理しやすいです。

カテゴリ ラベル例 用途
kind: kind: bug kind: feature kind: docs kind: chore 変更の種類
priority: priority: high priority: medium priority: low 優先度
status: status: in-progress status: blocked status: needs-review 進捗状態

labels.sh でテンプレート化する

カスタムラベルをシェルスクリプトにまとめておくと、新しいリポジトリに何度でも流用できます。

#!/bin/bash
# labels.sh
set -e

# kind
gh label create "kind: bug"      --color "d73a4a" --description "バグ・不具合"
gh label create "kind: feature"  --color "0075ca" --description "新機能・機能改善"
gh label create "kind: docs"     --color "0052cc" --description "ドキュメント"
gh label create "kind: chore"    --color "e4e669" --description "依存関係・設定変更"
gh label create "kind: refactor" --color "fbca04" --description "リファクタリング"

# priority
gh label create "priority: high"   --color "b60205" --description "優先度:高"
gh label create "priority: medium" --color "e99695" --description "優先度:中"
gh label create "priority: low"    --color "f9d0c4" --description "優先度:低"

# status
gh label create "status: in-progress"  --color "0e8a16" --description "作業中"
gh label create "status: blocked"      --color "ee0701" --description "ブロック中"
gh label create "status: needs-review" --color "c2e0c6" --description "レビュー待ち"
bash labels.sh

gh label create の主なオプションは以下のとおりです。

オプション 説明
--color 6 桁の16進カラーコード(# なし)
--description ラベルの説明文
--force 同名ラベルが存在する場合に上書き

別リポジトリへコピーする

既存リポジトリのラベルをそのまま別リポジトリへ移植するには、JSON でエクスポートしてスクリプトを生成する方法が便利です。

# コピー元リポジトリのラベルを取得してそのまま別リポジトリへ作成
gh label list --json name,color,description --repo owner/source-repo \
  | jq -r '.[] | "gh label create \"\(.name)\" --color \"\(.color)\" --description \"\(.description)\""' \
  | bash

--repo オプションでリポジトリを指定するため、ローカルに clone していなくても実行できます。


まとめ

やりたいこと コマンド
ラベルを一覧表示 gh label list
ラベルを作成 gh label create "名前" --color "色" --description "説明"
ラベルを削除 gh label delete "名前" --yes
全ラベルを一括削除 gh label list --json name | jq -r '.[].name' | while ... gh label delete
別リポジトリへコピー gh label list --json ... --repo owner/repo | jq ... | bash

デフォルトラベルを削除して labels.sh に自分たちの運用ルールを定義しておくと、新しいリポジトリを作るたびに bash labels.sh だけで同じラベルセットを再現できます。

GitHub の PR テンプレートについては「GitHub PR・ISSUEテンプレートの作り方:チームのレビュー効率を上げる設定まとめ」も参照してください。

useImperativeHandleで子コンポーネントにカスタムAPIを定義する

// 子コンポーネントが公開するAPIの型
export type InputFieldHandle = {
  focus: () => void
  clear: () => void
  getValue: () => string
}

// useImperativeHandle で外から呼べるメソッドを定義する
const InputField = forwardRef<InputFieldHandle, Props>((props, ref) => {
  const [value, setValue] = useState('')
  const inputRef = useRef<HTMLInputElement>(null)

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => { setValue('') },
    getValue: () => value,
  }))

  return <input ref={inputRef} value={value} onChange={(e) => setValue(e.target.value)} />
})

前の記事では forwardRef を使って親から子の DOM 要素に直接アクセスしました。今回はその発展として useImperativeHandle を紹介します。

forwardRef だけでは子の raw な DOM が露出しますが、useImperativeHandle を使うと子コンポーネントが「外から呼べるメソッド」を自分で定義できます。DOMの代わりに focus() / clear() / getValue() といったカスタム API を公開する形です。


構成

3つのコンポーネントで構成します。

  • InputField.tsxuseImperativeHandle でカスタムAPIを公開する子コンポーネント
  • FocusPanel.tsx:親から受け取ったrefを使って InputField を操作する子コンポーネント
  • App.tsx:ref を持ち、両コンポーネントをレンダリングする親コンポーネント

InputField.tsx:useImperativeHandle でAPIを定義する

まず公開するAPIの型を定義します。

export type InputFieldHandle = {
  focus: () => void
  clear: () => void
  getValue: () => string
}

次に forwardRefuseImperativeHandle を組み合わせます。

import { forwardRef, useImperativeHandle, useRef, useState } from 'react'

const InputField = forwardRef<InputFieldHandle, Props>(
  ({ label, placeholder, onFocus, onKeyDown }, ref) => {
    const [value, setValue] = useState('')
    const inputRef = useRef<HTMLInputElement>(null)

    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current?.focus(),
      clear: () => { setValue('') },
      getValue: () => value,
    }))

    return (
      <div className="field">
        <label>{label}</label>
        <input
          ref={inputRef}
          value={value}
          onChange={(e) => setValue(e.target.value)}
          onFocus={onFocus}
          onKeyDown={onKeyDown}
        />
      </div>
    )
  }
)

ポイントは2つです。

  • value の状態は InputField 自身が useState で管理します。親は state を持ちません
  • useImperativeHandle の第2引数に公開するメソッドを返す関数を渡します。外からは ref.current.focus() のように呼べます

App.tsx:ref の型が InputFieldHandle になる

親は useRef<InputFieldHandle>(null) で ref を作ります。HTMLInputElement ではなく InputFieldHandle 型になる点が forwardRef だけのときとの違いです。

const nameRef = useRef<InputFieldHandle>(null)
const emailRef = useRef<InputFieldHandle>(null)

送信時は getValue() で値を取得し、clear() でリセットします。

const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault()
  const name = nameRef.current?.getValue()
  const email = emailRef.current?.getValue()
  console.log({ name, email })
  emailRef.current?.clear()
  nameRef.current?.clear()
  nameRef.current?.focus()
}

FocusPanel.tsx:子コンポーネントから InputField を操作する

FocusPanelApp から nameRef / emailRef を props で受け取り、ボタン操作で InputField のメソッドを呼びます。

type Props = {
  nameRef: RefObject<InputFieldHandle | null>
  emailRef: RefObject<InputFieldHandle | null>
  lastFocusedRef: RefObject<InputFieldHandle | null>
}

function FocusPanel({ nameRef, emailRef, lastFocusedRef }: Props) {
  return (
    <div className="panel">
      {/* クリア後に対象フィールドへ移動 */}
      <button onClick={() => { nameRef.current?.clear(); nameRef.current?.focus() }}>
        名前をクリア
      </button>

      {/* クリア後に元のフィールドへ戻る */}
      <button onClick={() => { nameRef.current?.clear(); lastFocusedRef.current?.focus() }}>
        名前をクリア(元に戻る)
      </button>
    </div>
  )
}

フォーカス制御の2パターン

クリア後のフォーカス挙動は用途によって使い分けられます。

パターン1:クリア後に対象フィールドへ移動

clear() でリセットした直後に同じフィールドの focus() を呼ぶだけです。「クリアしたらそのフィールドを入力し直す」という流れに向いています。

// FocusPanel.tsx:クリア後に対象フィールドへ移動する
nameRef.current?.clear()
nameRef.current?.focus()

クリア後に対象フィールドへ移動

パターン2:クリア後に元のフィールドへ戻る

直前にフォーカスしていたフィールドを lastFocusedRef で追跡しておき、クリア後に戻します。

// App.tsx:onFocus でフォーカス位置を記録する
const lastFocusedRef = useRef<InputFieldHandle | null>(null)

<InputField
  ref={nameRef}
  onFocus={() => { lastFocusedRef.current = nameRef.current }}
  ...
/>
<InputField
  ref={emailRef}
  onFocus={() => { lastFocusedRef.current = emailRef.current }}
  ...
/>
// FocusPanel.tsx:クリア後に元の位置へ戻す
nameRef.current?.clear()
lastFocusedRef.current?.focus()

クリア後に元のフィールドへ戻る


forwardRef との違い

forwardRef のみ useImperativeHandle
親が取得するもの DOM 要素(HTMLInputElement カスタムAPI(InputFieldHandle
子の state 親が管理 子が自分で管理
公開する操作 DOM の全メソッド 定義したメソッドのみ
カプセル化 弱い 強い

useImperativeHandle は「子の内部実装を隠しながら、必要な操作だけを外に公開したい」場合に適しています。


まとめ

やりたいこと 使うもの ポイント
子にカスタムAPIを定義する useImperativeHandle 第2引数にメソッドを返す関数を渡す
子が自分で state を管理する useState を子の中に置く 親は state を持たない
直前のフォーカス位置を記録する useRef + onFocus クリア後の復元に使う
子から公開するAPIの型を定義する export type XxxHandle 親の useRef<XxxHandle> と合わせる

React入力フォームのフォーカス制御:useRef・forwardRefを使う

// 画面表示時に自動フォーカス
const nameRef = useRef<HTMLInputElement>(null)
useEffect(() => { nameRef.current?.focus() }, [])

// Enter で次フィールドへ移動
const moveFocus = (e: React.KeyboardEvent<HTMLInputElement>, next: React.RefObject<HTMLInputElement | HTMLButtonElement | null>) => {
  if (e.key === 'Enter') { e.preventDefault(); next.current?.focus() }
}

// 子コンポーネントに ref を渡す
const InputField = forwardRef<HTMLInputElement, Props>(({ label, onChange }, ref) => (
  <input ref={ref} onChange={onChange} />
))

Reactでフォームを作るとき、こんな操作を実現したい場面があると思います。

  • 画面表示と同時に最初の入力欄にフォーカスが当たる
  • Enterキーで次の入力欄に移動できる
  • 送信後にフォームがクリアされ、先頭の入力欄にフォーカスが戻る

キーボードだけで操作できるフォームはUXとして優しいですね。useRefuseEffectforwardRef を使って実装する方法を解説します。

完成形はこちらです。

React input フォーカス制御デモ


構成

2つのコンポーネントで構成します。

  • App.tsx:フォームのロジックとrefを管理する親コンポーネント
  • InputField.tsxforwardRef で ref を受け取る子コンポーネント

InputField.tsx:forwardRefで ref を受け取る

まず子コンポーネントから作ります。子コンポーネントの <input> に外から ref を渡したいときは forwardRef を使います。

import { forwardRef } from 'react'

type Props = {
  label: string
  value: string
  placeholder?: string
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
  onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
}

const InputField = forwardRef<HTMLInputElement, Props>(
  ({ label, value, placeholder, onChange, onKeyDown }, ref) => (
    <div className="field">
      <label>{label}</label>
      <input
        ref={ref}
        value={value}
        placeholder={placeholder}
        onChange={onChange}
        onKeyDown={onKeyDown}
      />
    </div>
  )
)

InputField.displayName = 'InputField'

export default InputField

forwardRef<HTMLInputElement, Props> の第1型引数が ref の型、第2引数が Props の型です。displayName を設定しておくと React DevTools で名前が表示されます。


App.tsx:フォーカス制御のロジック

useRef で DOM への参照を持つ

const nameRef = useRef<HTMLInputElement>(null)
const emailRef = useRef<HTMLInputElement>(null)
const submitRef = useRef<HTMLButtonElement>(null)

useRef<HTMLInputElement>(null) で入力欄への参照を作ります。ref.current から DOM 要素に直接アクセスできます。

useEffect で初期フォーカス

useEffect(() => {
  nameRef.current?.focus()
}, [])

依存配列を [] にすることで初回レンダリング時だけ実行されます。?. はオプショナルチェーニングで current が null のときは何もしません。

Enter で次フィールドへ移動

const moveFocus = (
  e: React.KeyboardEvent<HTMLInputElement>,
  next: React.RefObject<HTMLInputElement | HTMLButtonElement | null>
) => {
  if (e.key === 'Enter') {
    e.preventDefault()
    next.current?.focus()
  }
}

e.preventDefault() でブラウザのデフォルト動作(フォーム送信)を止めてから、次の要素にフォーカスを移します。移動先が input でも button でも同じ関数で動くよう、union 型で受け取っています。

送信後にフォーカスをリセット

const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault()
  setName('')
  setEmail('')
  nameRef.current?.focus()
}

setState でフォームをクリアした後、nameRef.current?.focus() で先頭の入力欄に戻します。

JSX で ref と onKeyDown を渡す

<InputField
  ref={nameRef}
  label="名前"
  value={name}
  placeholder="山田 太郎"
  onChange={(e) => setName(e.target.value)}
  onKeyDown={(e) => moveFocus(e, emailRef)}
/>
<InputField
  ref={emailRef}
  label="メールアドレス"
  value={email}
  placeholder="example@email.com"
  onChange={(e) => setEmail(e.target.value)}
  onKeyDown={(e) => moveFocus(e, submitRef)}
/>
<button ref={submitRef} type="submit">
  送信
</button>

onKeyDown に次の移動先 ref を渡します。名前フィールドは emailRef、メールフィールドは submitRef へ。


App.tsx 全体

import { useEffect, useRef, useState } from 'react'
import InputField from './InputField'

function App() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')

  const nameRef = useRef<HTMLInputElement>(null)
  const emailRef = useRef<HTMLInputElement>(null)
  const submitRef = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    nameRef.current?.focus()
  }, [])

  const moveFocus = (
    e: React.KeyboardEvent<HTMLInputElement>,
    next: React.RefObject<HTMLInputElement | HTMLButtonElement | null>
  ) => {
    if (e.key === 'Enter') {
      e.preventDefault()
      next.current?.focus()
    }
  }

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    setName('')
    setEmail('')
    nameRef.current?.focus()
  }

  return (
    <div className="container">
      <h1>お問い合わせ</h1>
      <form onSubmit={handleSubmit}>
        <InputField
          ref={nameRef}
          label="名前"
          value={name}
          placeholder="山田 太郎"
          onChange={(e) => setName(e.target.value)}
          onKeyDown={(e) => moveFocus(e, emailRef)}
        />
        <InputField
          ref={emailRef}
          label="メールアドレス"
          value={email}
          placeholder="example@email.com"
          onChange={(e) => setEmail(e.target.value)}
          onKeyDown={(e) => moveFocus(e, submitRef)}
        />
        <button ref={submitRef} type="submit">
          送信
        </button>
      </form>
    </div>
  )
}

export default App

まとめ

やりたいこと 使うもの ポイント
画面表示時に自動フォーカス useRef + useEffect(fn, []) 依存配列を空にして初回のみ実行
Enterキーで次フィールドへ onKeyDown + e.preventDefault() デフォルトのフォーム送信を止めてから移動
送信後にフォーカスをリセット handleSubmit 内で .focus() setState の後に呼ぶ
子コンポーネントに ref を渡す forwardRef 第1型引数に ref の型、第2引数に Props の型

miseのdotfiles機能:.zshrcや.gitconfigを複数PCで統一する

miseのdotfiles機能:.zshrcや.gitconfigを複数PCで統一する

# ~/.config/mise/config.toml
[dotfiles]
root = "~/.dotfiles"

["~/.zshrc"]
["~/.gitconfig"]
mise dotfiles apply   # シンボリックリンクを展開して設定を適用

mise はバージョン管理だけでなく、dotfiles(設定ファイル)の管理もできます。.zshrc.gitconfig を Git リポジトリで管理しておき、mise dotfiles apply でシンボリックリンクを張ることで、複数のマシンで同じ設定を再現できます。chezmoi や GNU Stow に近い概念ですが、すでに mise を使っているなら追加インストール不要で使えます。


セットアップ

1. dotfiles リポジトリを用意する

管理したい設定ファイルを置くディレクトリを作ります。Git リポジトリにしておくと複数マシンへの配布が簡単です。

mkdir ~/.dotfiles
cd ~/.dotfiles
git init

2. 既存ファイルを管理対象に追加する

mise dotfiles add を使うと、ファイルが ~/.dotfiles/ に移動されて元の場所にシンボリックリンクが作成されます。

mise dotfiles add ~/.zshrc
mise dotfiles add ~/.gitconfig

手動で移動する場合はこのようにします。

mv ~/.zshrc ~/.dotfiles/zshrc
mv ~/.gitconfig ~/.dotfiles/gitconfig

3. mise の設定に登録する

~/.config/mise/config.toml[dotfiles] セクションを追加します。

[dotfiles]
root = "~/.dotfiles"

["~/.zshrc"]
["~/.gitconfig"]

キーにターゲットのパス(~/.zshrc)を指定します。値を省略すると、dotfiles.root 内の同名ファイル(~/.dotfiles/zshrc)が自動的にソースになります。

4. 適用する

mise dotfiles apply

~/.zshrc~/.dotfiles/zshrc のシンボリックリンクが作成されます。


状態を確認する

mise dotfiles status

管理対象ファイルの一覧と、リンクが正しく張られているかを確認できます。


apply 前に dry-run で確認する

mise dotfiles apply --dry-run

実際には何も変更せず、何が行われるかを事前に確認できます。初回は必ず dry-run で確認してから実行するのがおすすめです。


ソースパスを明示する

サブディレクトリに整理している場合はパスを明示します。

[dotfiles]
root = "~/.dotfiles"

["~/.zshrc"] = "zsh/zshrc"
["~/.gitconfig"] = "git/gitconfig"
["~/.config/starship.toml"] = "starship/starship.toml"

適用モード

デフォルトは symlink です。用途に応じて変更できます。

モード 説明 向いているケース
symlink(デフォルト) ソースへのシンボリックリンクを作成 編集がリポジトリに即反映されてほしい
symlink-each ディレクトリ内の各ファイルを個別にリンク ~/.config/ 配下の一部だけ管理したい
copy ファイルをコピー シンボリックリンクを受け付けないツール用
template テンプレートとして処理して出力 マシンごとに内容を変えたい

モードはファイルごとに指定できます。

["~/.gitconfig"] = { mode = "copy" }

新しいマシンでのセットアップ

dotfiles を Git で管理しておくと、新しいマシンのセットアップが以下の手順だけで済みます。

# 1. dotfiles リポジトリを clone
git clone git@github.com:yourname/dotfiles.git ~/.dotfiles

# 2. mise をインストール
curl https://mise.run | sh

# 3. ~/.config/mise/config.toml を配置して apply
mise dotfiles apply

まとめ

コマンド 説明
mise dotfiles apply シンボリックリンクを作成して設定を展開
mise dotfiles apply --dry-run 変更内容をプレビュー(実行はしない)
mise dotfiles apply --force 既存ファイルを上書きして強制適用
mise dotfiles status 管理対象ファイルの状態を確認
mise dotfiles add ~/.zshrc 既存ファイルを管理対象に追加
mise dotfiles edit ~/.zshrc 管理対象のソースファイルを編集

mise で言語バージョンも dotfiles も一元管理することで、新しいマシンのセットアップが大幅に楽になります。

mise のインストールや言語バージョン管理については「anyenv から mise に移行:Node.js・Ruby・Python の管理方法」も参照してください。

JavaScriptで配列の重複を除去する:Set・filter・reduceの使い方

JavaScriptで配列の重複を除去する:Set・filter・reduceの使い方

const unique = [...new Set(array)];

最もシンプルな方法です。Set は重複を持たないため、一度変換してスプレッド構文で配列に戻すだけで重複が除去できます。


はじめに

JavaScriptで配列から重複した値を取り除いてユニークな配列を作る方法をまとめます。Set を使う方法が最もシンプルですが、用途によって使い分けがあります。


Set を使う(最もシンプル)

Set は重複を持たないコレクションです。配列を Set に変換してから配列に戻すことで重複を除去できます。

const numbers = [1, 1, 1, 5, 5, 3, 2, 10, 9];

const unique = [...new Set(numbers)];
console.log(unique); // [1, 5, 3, 2, 10, 9]

Array.from を使う書き方も同じ結果です。

const unique = Array.from(new Set(numbers));

文字列の配列でも同様に使えます。

const fruits = ['apple', 'banana', 'apple', 'orange', 'banana'];
const unique = [...new Set(fruits)];
console.log(unique); // ['apple', 'banana', 'orange']

注意点:挿入順が保持される

Set は挿入順を保持します。ソートはされません。

const nums = [3, 1, 2, 1, 3];
console.log([...new Set(nums)]); // [3, 1, 2](並び順は変わらない)

filter + indexOf を使う

indexOf は最初に一致したインデックスを返すため、2回目以降の出現を除外できます。

const numbers = [1, 1, 5, 5, 3, 2];

const unique = numbers.filter((v, i) => numbers.indexOf(v) === i);
console.log(unique); // [1, 5, 3, 2]
  • シンプルだが、indexOf が O(n) なので大きな配列では Set より遅い

reduce を使う

const numbers = [1, 1, 5, 5, 3, 2];

const unique = numbers.reduce((acc, v) => {
  return acc.includes(v) ? acc : [...acc, v];
}, []);

console.log(unique); // [1, 5, 3, 2]

オブジェクトの配列を重複除去する

Set はオブジェクトの参照で比較するため、中身が同じでも別インスタンスなら重複と見なしません。

const users = [
  { id: 1, name: '田中' },
  { id: 2, name: '鈴木' },
  { id: 1, name: '田中' }, // id が重複
];

// Set では除去できない(参照が異なるため)
console.log([...new Set(users)].length); // 3(除去されない)

// 特定のキーで重複除去する場合
const uniqueById = users.filter(
  (user, i, arr) => arr.findIndex(u => u.id === user.id) === i
);
console.log(uniqueById);
// [{ id: 1, name: '田中' }, { id: 2, name: '鈴木' }]

Map を使う方法もあります。

const uniqueById = [
  ...new Map(users.map(u => [u.id, u])).values()
];

方法の比較

方法 簡潔さ 速度 用途
Set 速い プリミティブ値の重複除去
filter + indexOf 遅い(大配列) シンプルな条件除去
reduce 遅い カスタム処理が必要な場合
Map 速い オブジェクトをキーで重複除去

まとめ

// 数値・文字列の重複除去(推奨)
const unique = [...new Set(array)];

// オブジェクトをキーで重複除去
const unique = [...new Map(arr.map(x => [x.key, x])).values()];

プリミティブ値(数値・文字列)の重複除去は Set が最もシンプルで高速です。オブジェクトの配列は特定のキーで比較する必要があるため filter + findIndexMap を使います。

JavaScriptの配列メソッド(map・filter・reduce)の詳細は「JavaScriptのmap・filter・flatMap・reduce入門:メソッドチェーンまで」を参照してください。

JavaScriptの正規表現については「JavaScriptの正規表現入門:基本パターンからフラグ・よく使う使い方まとめ」を参照してください。

配列のソート(複数条件ソートも含む)については「JavaScriptのArray.sort()で複数条件ソートする方法:数値・文字列・多段ソート」を参照してください。

mise で Ruby をインストールする方法:.ruby-version の使い方

mise で Ruby をインストールする方法:.ruby-version の使い方

mise install ruby@3.3.0   # インストール
mise use ruby@3.3.0       # プロジェクトに固定(mise.toml に書き込まれる)
ruby --version            # 確認

mise はバージョン管理ツールで、Ruby を含む複数の言語をまとめて管理できます。この記事では mise を使った Ruby のインストールから、プロジェクトごとのバージョン固定、.ruby-version ファイルとの連携までを解説します。


Ruby をインストールする

# 最新の安定版をインストール
mise install ruby

# バージョンを指定してインストール
mise install ruby@3.3.0
mise install ruby@3.2.4

# インストール可能なバージョン一覧を確認
mise ls-remote ruby

インストール後、ruby コマンドが使えるようになります。

ruby --version
# => ruby 3.3.0 (...)

インストール済みバージョンを確認する

# インストール済みの Ruby 一覧
mise list ruby

# 現在有効なバージョン
mise current ruby

プロジェクトのバージョンを固定する

mise use で固定(推奨)

# カレントディレクトリに固定(mise.toml に書き込まれる)
mise use ruby@3.3.0

# グローバルに固定
mise use --global ruby@3.3.0

mise use を実行すると mise.toml が生成・更新されます。

# mise.toml
[tools]
ruby = "3.3.0"

このファイルをコミットしておくと、チームメンバーが mise install を実行するだけで同じバージョンが使えます。


.ruby-version ファイルとの連携

mise は .ruby-version ファイルを自動で読み込みます。rbenv を使っていたプロジェクトでも、そのまま mise で使えます。

# .ruby-version ファイルを作成
echo "3.3.0" > .ruby-version
# .ruby-version に書かれたバージョンをインストール
mise install

.ruby-versionmise.toml が両方ある場合は mise.toml が優先されます。


バージョンを切り替える

# シェルセッション中だけ切り替え
mise shell ruby@3.2.4

# プロジェクトのバージョンを変更
mise use ruby@3.2.4

まとめ

コマンド 説明
mise install ruby 最新の安定版をインストール
mise install ruby@3.3.0 バージョンを指定してインストール
mise list ruby インストール済みバージョン一覧
mise current ruby 現在有効なバージョンを確認
mise use ruby@3.3.0 プロジェクトのバージョンを固定(mise.toml)
mise use --global ruby@3.3.0 グローバルバージョンを固定
mise shell ruby@3.2.4 セッション中だけ切り替え
mise install .ruby-version / mise.toml のバージョンをインストール

.ruby-version を使えば rbenv からの移行もスムーズです。mise のインストール方法や rbenv との使い分けについては「Rubyのバージョン管理:rbenv・miseの使い方と切り替え方法」も参照してください。

Node.js も mise で管理する場合は「mise で Node.js をインストールする方法:.nvmrc の使い方」も参照してください。

RSpecのlet_it_be:test-profで高速化するDB生成の使い方

RSpecのlet_it_be:test-profで高速化するDB生成の使い方

# let! を let_it_be に変えるだけ
let_it_be(:user) { create(:user) }   # グループ全体で1回だけ生成(let!はitの数だけ生成)

let! との違い:let! は各 it の前に毎回DBへINSERTします。let_it_be はexample group内で1回だけINSERTし、テスト間の副作用はトランザクションで隔離します。


はじめに

RSpecのテストでDBへのレコード生成が多くなると、let! の繰り返し実行がボトルネックになります。let_it_betest-prof gemが提供するヘルパーで、同じexample group内でレコードを1回だけ生成してテストを高速化できます。


let! の何が遅いか

let! は各 it の前に毎回評価されます。

RSpec.describe Post, type: :model do
  let!(:user)     { create(:user) }     # 各itの前に実行
  let!(:category) { create(:category) } # 各itの前に実行
  let!(:post)     { create(:post, user: user, category: category) }

  it "タイトルを持つ"  do ... end  # ← user, category, post を生成
  it "公開できる"      do ... end  # ← user, category, post を生成(また)
  it "下書きに戻せる"  do ... end  # ← user, category, post を生成(また)
end

itが3つあれば3回、10個あれば10回DBへのINSERTが発生します。大きなテストスイートではこれが積み重なり、実行時間が長くなります。


let_it_be の仕組み

let_it_be はexample group(describecontext)単位で1回だけレコードを生成し、各 it 間はトランザクションでラップして副作用を隔離します。

RSpec.describe Post, type: :model do
  let_it_be(:user)     { create(:user) }     # グループ全体で1回だけ生成
  let_it_be(:category) { create(:category) }
  let_it_be(:post)     { create(:post, user: user, category: category) }

  it "タイトルを持つ"  do ... end  # ← 生成済みのレコードを使う
  it "公開できる"      do ... end  # ← 生成済みのレコードを使う
  it "下書きに戻せる"  do ... end  # ← 生成済みのレコードを使う
end

itが3つでも10個でも、INSERTは1回ずつです。


インストール

test-prof gemをGemfileに追加します。

# Gemfile
group :test do
  gem 'test-prof'
end
bundle install

spec/spec_helper.rb または spec/rails_helper.rb に設定を追加します。

# spec/rails_helper.rb
require 'test_prof/recipes/rspec/let_it_be'

基本的な使い方

let!let_it_be に置き換えるだけで使えます。

RSpec.describe User, type: :model do
  let_it_be(:user) { create(:user, name: "田中太郎") }

  it "名前を持つ" do
    expect(user.name).to eq("田中太郎")
  end

  it "有効である" do
    expect(user).to be_valid
  end
end

ネストした context でも使える

RSpec.describe Post, type: :model do
  let_it_be(:user) { create(:user) }

  context "公開状態の場合" do
    let_it_be(:post) { create(:post, user: user, published: true) }

    it "公開されている" do
      expect(post.published?).to be true
    end
  end

  context "下書き状態の場合" do
    let_it_be(:post) { create(:post, user: user, published: false) }

    it "非公開である" do
      expect(post.published?).to be false
    end
  end
end

注意点:デフォルトでimmutable

let_it_be で生成したオブジェクトはデフォルトで変更不可(frozen)扱いになります。テスト内でオブジェクトの属性を変更しても、他のテストには影響しません。

let_it_be(:user) { create(:user, name: "田中太郎") }

it "名前を変更しても他のテストに影響しない" do
  user.name = "鈴木花子"   # メモリ上の変更
  expect(user.name).to eq("鈎木花子")
end

it "元の名前のまま" do
  expect(user.name).to eq("田中太郎")   # 影響を受けない
end

DBを更新した場合は reload: true

テスト内でDBを直接更新し、次のテストでもDBの状態を反映させたい場合は reload: true を使います。

let_it_be(:user, reload: true) { create(:user, name: "田中太郎") }

it "DBを更新する" do
  user.update!(name: "鈴木花子")
  # 次のit開始前にuser.reloadが自動で呼ばれる
end

it "常にDBの最新状態を参照する" do
  expect(user.name).to eq("田中太郎")   # reloadされて元の値に戻っている
end

再取得が必要な場合は refind: true

オブジェクトをDBから再取得(新しいインスタンスとして)したい場合は refind: true を使います。

let_it_be(:post, refind: true) { create(:post) }

reload: true は同じオブジェクトをリロード、refind: true は新しいオブジェクトとして取得し直す違いがあります。


let・let!・let_it_be の比較

let let! let_it_be
評価タイミング 初めて呼ばれたとき 各itの前 グループで1回だけ
DB生成回数 呼ばれた回数 itの数 × 1回 1回
it間の副作用 なし トランザクションでロールバック トランザクションでロールバック
向いているケース 軽い計算・オブジェクト DBが必要な前提条件 変更しない共通レコード

どこに使うか

let_it_be はすべてのケースで使えるわけではありません。

向いているケース: - テスト内でレコードを更新・削除しない共通の前提データ - 親レコード(User・Categoryなど)の生成

向いていないケース: - テスト内でレコードを更新・削除する場合(let!reload: true を使う) - テストごとに異なる状態のレコードが必要な場合

基本は let を使い、DBレコードが必要で変更しない共通データは let_it_be、テストごとに状態が変わるものは let! という使い分けが実践的です。


まとめ

  • let_it_be はexample group単位でDBレコードを1回だけ生成する
  • let! の代替として使うとテストの実行速度が大幅に改善できる
  • デフォルトでimmutableなため他のテストへの副作用がない
  • DBを更新する場合は reload: true、再取得は refind: true を使う

letlet! の基本的な違いは「RSpecのletとlet!:遅延評価と即時評価の違いと使い分け」を参照してください。

RSpecの基本的な書き方は「RSpec入門:インストールからモデルスペックの書き方まで」を参照してください。