0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Conventional Commits から CHANGELOG.md を自動生成する Rust CLI を作った

0
Last updated at Posted at 2026-05-04

きっかけ

「今回のリリースで何が変わったの?」——この質問にチームの誰かが毎回 git log を遡って手作業で答えている。Conventional Commits を使っていれば答えはコミット履歴に埋まっているのに、抽出する手段が面倒すぎる。

既存の conventional-changelog (Node) や git-cliff (Rust) は高機能だけど、設定ファイルやプラグインが前提。欲しいのは「リポジトリで実行したら即 CHANGELOG が出る」ゼロ設定のツール。というわけで changelog-rs を書きました。

📦 GitHub: https://github.com/sen-ltd/changelog-rs

スクリーンショット

作ったもの

changelog-rsgit log の出力をパースし、Conventional Commits のサブジェクトをタイプ別にグループ化して、Keep a Changelog 形式の Markdown を生成する Rust CLI です。

# リポジトリで実行するだけ
$ changelog-rs --version 2.0.0

# 特定の範囲だけ
$ changelog-rs --from v1.0.0 --to HEAD --version 2.0.0

# stdin からパイプも可
$ git log --format="%H %s" v1.0.0..HEAD | changelog-rs --stdin

依存は clap(引数パース)と regex(コミットパース)の 2 つだけ。リリースバイナリは約 1 MB。

技術的なポイント

アーキテクチャ:3 つの純粋関数

ライブラリ全体が 3 関数に収まっています。意図的にこの構造にしました。

1. パース — 1 行を受け取り Option<Commit> を返す:

pub fn parse_commit(line: &str) -> Option<Commit> {
    let line = line.trim();
    if line.is_empty() {
        return None;
    }

    let (hash, subject) = split_hash_subject(line)?;

    let re = Regex::new(
        r"^(feat|fix|refactor|perf|docs|test|tests|build|ci|style|chore)(\([^)]+\))?(!)?\s*:\s*(.+)$"
    ).unwrap();

    let caps = re.captures(subject)?;
    // ... フィールド抽出、Some(Commit { ... }) を返す
}

Conventional Commits に合致しないコミット(マージコミット、WIP など)は None を返すだけ。Result ではなく Option を使うのがポイントで、非 conventional なコミットはエラーではなく「期待される入力」だからです。

2. グループ化BTreeMap でタイプ別に分類:

pub fn group_commits(commits: &[Commit]) -> BTreeMap<Category, Vec<Commit>> {
    let mut groups: BTreeMap<Category, Vec<Commit>> = BTreeMap::new();
    for commit in commits {
        groups.entry(commit.category).or_default().push(commit.clone());
    }
    groups
}

HashMap ではなく BTreeMap を使う理由は、セクションの出力順を決定的にするため。Added → Fixed → Refactored の順番が毎回同じになり、リリース間で changelog をスキャンしやすくなります。

3. レンダリング — Keep a Changelog 形式の Markdown を生成:

Breaking changes は元のコミットタイプに関係なく、専用セクションに切り出して先頭に配置。feat!: drop Node 14 support は Breaking Changes セクション Added セクションの両方に出ます。

依存を最小限に

chrono を入れれば日付フォーマットは楽ですが、200+ の推移的依存が付いてくる。必要なのは YYYY-MM-DD 形式の今日の日付だけなので、システムの date コマンドにシェルアウトしました。実用上の問題はゼロで、バイナリサイズとコンパイル時間を大幅に節約。

[profile.release]
strip = true
lto = true
codegen-units = 1
opt-level = "z"
panic = "abort"

テスト戦略

20 テストを 3 層で構成:

  • パーステスト — 全 10 タイプの認識、スコープ抽出、!BREAKING CHANGE: 両方の破壊的変更検出、空行やハッシュなしの入力
  • グループ化テスト — カテゴリ分配の正確性、空入力
  • レンダリングテスト — Markdown 出力全体の検証(ヘッダ、バージョンセクション、スコープ書式、ハッシュ書式)
#[test]
fn test_parse_feat_with_scope() {
    let c = parse_commit("abc1234 feat(parser): add JSON support").unwrap();
    assert_eq!(c.category, Category::Feat);
    assert_eq!(c.scope.as_deref(), Some("parser"));
    assert_eq!(c.description, "add JSON support");
    assert!(!c.breaking);
}

使い方

# 基本:未リリースの changelog を生成
$ changelog-rs

# バージョン付き(今日の日付が自動挿入)
$ changelog-rs --version 2.0.0

# 日付を指定
$ changelog-rs --version 2.0.0 --date 2026-04-01

# ファイルに書き出し
$ changelog-rs --version 2.0.0 -o CHANGELOG.md

# コミットハッシュ付き
$ changelog-rs --hashes

Docker でも使えます:

docker run --rm -v "$(pwd):/work" changelog-rs --version 1.0.0

おわりに

changelog 生成は「あると便利だけど毎回面倒」の典型。Conventional Commits を使っているなら、コミット履歴が既に構造化されたデータなので、パースしてフォーマットするだけ。3 関数、2 依存、1 MB のバイナリで十分実用になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?