pre-commitフックでコピーライトをチェックする

というわけで、コピーライトのチェックをするSubversion pre-commitフックを書いてみました。
Windowsのバッチファイル中でコマンド置換する方法が分からなかったのでC++で書きました。

#include <hamigaki/process/child.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/iostreams/detail/ios.hpp>
#include <boost/iostreams/filter/newline.hpp>
#include <boost/iostreams/filtering_stream.hpp>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <limits>

namespace algo = boost::algorithm;
namespace io = boost::iostreams;
namespace proc = hamigaki::process;

// テキストファイルの拡張子一覧(ソート済み)
const char* text_exts[] =
{
    "css",
    "h",
    "html",
    "jam",
    "pl",
    "rc",
    "txt",
    "v2",
    "xml"
};

struct c_str_less
{
    bool operator()(const char* lhs, const char* rhs) const
    {
        return std::strcmp(lhs, rhs) < 0;
    }
};

bool is_text(const std::string& ph)
{
    std::string name;
    std::string::size_type slash = ph.rfind('/');
    if (slash != std::string::npos)
        name.assign(ph, slash+1, std::string::npos);

    if (name == "LICENSE_1_0.txt")
        return false;

    if (name == "Jamfile")
        return true;

    std::string::size_type dot = name.rfind('.');
    if (slash == std::string::npos)
        return false;

    std::string ext(name, dot+1);
    if (std::binary_search(
        text_exts,
        text_exts+sizeof(text_exts)/sizeof(text_exts[0]),
        ext.c_str(),
        c_str_less()))
    {
        return true;
    }

    // Boostでは拡張子がppで終わるファイルはC++コードとみなす
    return algo::ends_with(ext, "pp");
}

bool check_file(
    const std::string& svnlook,
    const std::string& repos, const std::string& txn, const std::string& ph)
{
    // チェック不要なファイルをスキップする
    // 1/3更新: ファイル名のチェックを別関数に分離
    if (!is_text(ph))
        return true;

    // 「svnlook cat」でコミットしようとしている内容を確認する
    std::vector<std::string> args;
    args.push_back("svnlook");
    args.push_back("cat");
    args.push_back("-t");
    args.push_back(txn);
    args.push_back(repos);
    args.push_back(ph);

    proc::context ctx;
    ctx.stdin_behavior(proc::silence_stream());
    ctx.stdout_behavior(proc::capture_stream());
    ctx.stderr_behavior(proc::silence_stream());

    proc::child c(svnlook, args, ctx);
    io::filtering_stream<io::input> in;
    in.push(io::newline_filter(io::newline::posix));
    in.push(c.stdout_source());

    std::string line;
    while (std::getline(in, line))
    {
        // いい加減なチェック
        // 「2008」を含む行があればOK
        if (line.find("2008") != std::string::npos)
        {
            // 1/3更新: svnlookをTerminateProcess()すると
            // トランザクションが残るので、残りを読み飛ばし、終了を待つ
            in.ignore(std::numeric_limits<std::streamsize>::max());
            c.wait();
            return true;
        }
    }
    c.wait();

    return false;
}

bool check_changed(
    const std::string& svnlook,
    const std::string& repos, const std::string& txn)
{
    std::vector<std::string> files;
    {
        // 「svnlook changed」でコミットしようとしているファイル一覧を取得する
        std::vector<std::string> args;
        args.push_back("svnlook");
        args.push_back("changed");
        args.push_back("-t");
        args.push_back(txn);
        args.push_back(repos);

        proc::context ctx;
        ctx.stdin_behavior(proc::silence_stream());
        ctx.stdout_behavior(proc::capture_stream());
        ctx.stderr_behavior(proc::silence_stream());

        proc::child c(svnlook, args, ctx);
        io::filtering_stream<io::input> in;
        in.push(io::newline_filter(io::newline::posix));
        in.push(c.stdout_source());

        std::string line;
        while (std::getline(in, line))
        {
            // 削除とディレクトリは無視
            if ((line[0] == 'D') || algo::ends_with(line, "/"))
                continue;

            files.push_back(line.substr(4));
        }
        c.wait();
    }

    // 1/3更新: 違反ファイルの名前を標準エラー出力に書き出すように変更
    bool result = true;
    for (std::size_t i = 0; i < files.size(); ++i)
    {
        if (!check_file(svnlook, repos, txn, files[i]))
        {
            result = false;
            std::cerr << "Error: " << files[i] << std::endl;
        }
    }

    return result;
}

int main(int argc, char* argv[])
{
    try
    {
        // 適当
        std::string svnlook = "C:\\Program Files\\Subversion\\bin\\svnlook.exe";
        std::string repos = argv[1];
        std::string txn = argv[2];

        return check_changed(svnlook, repos, txn) ? 0 : 1;
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }
    return 1;
}

書いてから、istream_line_iteratorの存在を思い出しました。
まぁ、いいです。


(1/3 追記)
トランザクションが残ってコミットに失敗することがあったので修正。