「gitignore対象をSkip」フィルタ機能追加

TresGrep Ver.1.13にて、.gitignoreによりGit管理外としたファイル等を検索対象外とする「gitignore対象をSkip」フィルタを追加しました。
f:id:hnx8:20190202142944p:plain
Gitをインストールして開発作業等を行っている方向けの機能です。gitignoreの設定がしっかりしていれば、開発言語/開発環境などに応じたフィルタをいちいち作らずとも、必要なファイルだけを検索できるようになるはずです。
2019/02/06(水)付でVectorライブラリに反映され、ダウンロードできるようになる予定です。

gitignore対象判定のソースコード/リアルタイム検索時の制約について

gitignore対象か否かの判定のソースコードを(一部Blog掲載用に手直ししたうえで)公開します。
判定には「git check-ignore」コマンドを利用しています。「TresGrepでgitignore指定ファイルを検索対象外 - hnx8のブログ」「C#で特定フォルダ内のgitignore対象ではないファイル/サブフォルダを一括で取得する - hnx8のブログ」で紹介したソースコードをベースとしていますが、TresGrepのようなGrepツールに組み込むにあたってはgit check-ignoreコマンドの実行所要時間がかかりすぎて実用に耐えなかったため、以下のようなソースコードでgitignore判定結果をキャッシュするように実装してあります。(初回検索時はどうしても遅くなってしまいますが、いったん検索実行した後のリアルタイム検索ではキャッシュをもとにgitignore判定を行うので通常通りの検索パフォーマンスが得られます)

「リアルタイム検索」ONで即時検索している場合は、.gitignoreファイルの書き換えなどにより無視すべきファイルの範囲が変わっても、TresGrepの検索結果にはただちは反映されません。
(1) 「検索実行」ボタンをクリックして明示的に検索をやり直す
(2) フィルタ「gitignore対象をSkip」を選択しなおす
(3) TresGrepを一定時間(3分)なにも操作せず放置する ←Ver1.15(2019.06.07)で取りやめ
(4) TresGrepをいったん終了・再起動する
のいずれかの操作を行うと、最新の.gitignoreの設定がTresGrepにも反映されます。

/// <summary>
/// C#で特定のファイル/フォルダが.gitignore対象となっているか判定(判定結果キャッシュあり)
/// </summary>
class GitIgnoreChecker
{
    /// <summary>
    /// 引数で指定されたファイル/ディレクトリがgitignore対象ならtrueを返します。
    /// </summary>
    /// <param name="fsi">調べる対象のファイル/ディレクトリ</param>
    /// <returns>gitignore対象ならtrue</returns>
    public bool IsGitignored(FileSystemInfo fsi)
    {
        // 一定時間(既定では3分)経過したら調査結果のキャッシュを初期化しなおす
        DateTime now = DateTime.Now;
        if (now - LastAccessTime > TimeSpan.FromMinutes(GitIgnore_CacheExpireMinutes))
        {
            ClearCache();
        }
        LastAccessTime = now;

        // 探索対象ディレクトリを把握
        string dirName = Path.GetDirectoryName(fsi.FullName);
        if (dirName == null)
        {   // ただしC:\などのルートディレクトリはGetDirectoryName()結果がnullとなる。gitignore対象ではないとみなす
            return false;
        }
        // そのディレクトリのGitIgnore対象情報を把握(※ディレクトリ名のKeyはすべて小文字としておく)
        Dictionary<string, bool> gitignoreMap;
        if (!CacheMap.TryGetValue(dirName.ToLower(), out gitignoreMap) || (gitignoreMap != null && !gitignoreMap.ContainsKey(fsi.Name)))
        {   // 初出のディレクトリ or キャッシュにそのファイルのgitignore情報がなかった場合は、Mapを生成して保存
            gitignoreMap = CreateGitignoreMap(dirName);
            CacheMap[dirName.ToLower()] = gitignoreMap;
        }

        // gitignore対象ならtrueを返す
        bool ignored;
        return (gitignoreMap != null && gitignoreMap.TryGetValue(fsi.Name, out ignored) && ignored);
    }

    /// <summary>gitignore判定に使用するgit.exeのフルパス</summary>
    /// <remarks>仮に直値としてある。gitインストール先に応じた値を設定すること。</remarks>
    public static string GitExe_Path = @"C:\Program Files\Git\cmd\git.exe";

    /// <summary>gitignore判定結果キャッシュの有効期限</summary>
    /// <remarks>仮に直値としてある。適切な値を設定すること。</remarks>
    public static int GitIgnore_CacheExpireMinutes = 180;

    /// <summary>直前にIsGitignored()メソッドが呼び出された際の時刻</summary>
    /// <remarks>CacheMapを一定時間経過後にクリアするため使用</remarks>
    private DateTime LastAccessTime = DateTime.MinValue;

    /// <summary>
    /// 各ディレクトリのgitignore対象調査結果をキャッシュするためのMap
    /// (key=親ディレクトリ名(フルパス,すべて小文字)、value=(key=ファイル名,value=gitignore対象か否かを表すMap))
    /// </summary>
    /// <remarks>一定時間経過後にクリアされる</remarks>
    private ConcurrentDictionary<string, Dictionary<string, bool>> CacheMap;

    /// <summary>
    /// 各ディレクトリのgitignore対象調査結果をキャッシュするためのMapを初期化クリアします。
    /// </summary>
    public void ClearCache()
    {
        CacheMap = new ConcurrentDictionary<string, Dictionary<string, bool>>();
    }

    /// <summary>
    /// そのディレクトリ内の全ファイル/サブディレクトリについてgitignore対象であるか否かを調べ、Mapにして返します。
    /// </summary>
    /// <param name="dirName">ディレクトリ名フルパス</param>
    /// <returns>
    /// key=パスなしファイル名/サブディレクトリ名、value=gitignore対象ならtrue/否ならfalse、のMap。
    /// ただしそのディレクトリがgit管理外等でgitignore対象か否かを把握できなかった場合は、Mapのかわりにnullを返します。
    /// </returns>
    private Dictionary<string, bool> CreateGitignoreMap(string dirName)
    {
        // そのディレクトリ内の全エントリを、親パスなしにしてlistにつめる
        List<string> entries = new List<string>();
        foreach (string path in Directory.GetFileSystemEntries(dirName))
        {
            entries.Add(Path.GetFileName(path));
        }
        // git check-ignoreコマンドでgitignore対象となっているファイル等を調べる。対象はstdinから指定
        ProcessStartInfo psInfo = new ProcessStartInfo(GitExe_Path, "check-ignore --stdin")
        {
            CreateNoWindow = true,
            UseShellExecute = false,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            WorkingDirectory = dirName,
        };
        using (Process p = Process.Start(psInfo))
        {
            // stdoutから無視すべきファイルの一覧をOutputDataReceivedイベントで受け取れるようにし、listに蓄積する
            List<string> ignoredNames = new List<string>();
            p.OutputDataReceived += (sender, e) =>
            {
                if (!string.IsNullOrEmpty(e.Data)) { ignoredNames.Add(e.Data); }
            };
            p.BeginOutputReadLine();
            // 調べる対象のファイルをすべてstdinで引き渡す(gitignore対象ならstdoutにそのファイル名が出力される)
            using (StreamWriter sw = p.StandardInput)
            {
                foreach (string entry in entries)
                {
                    sw.Write(entry + "\n");
                }
            }
            // コマンド実行終了まで待ち、最悪でも10秒で強制終了
            if (!p.WaitForExit(10000))
            {
                p.Kill();
            }
            p.CancelOutputRead();
            // 戻り値を決定
            if (p.ExitCode <= 1)
            {   // gitコマンド成功
                Dictionary<string, bool> ret = new Dictionary<string, bool>();
                foreach (string entry in entries)
                {   // ※なお、.gitディレクトリはcheck-ignoreコマンドではgitignore対象には含まれていないが、無視するディレクトリであるとみなす。
                    ret[entry] = (ignoredNames.Contains(entry) || entry == ".git");
                }
                return ret;
            }
            else
            {   // gitコマンド失敗、git管理外のフォルダであるとみなす
                return null;
            }
        }
    }
}

ソースコードの利用・改変・移植はご自由にどうぞ。
またどなたか、WinMerge でgitignore指定ファイルを比較対象外にするようなプラグインを作れる方がいたら(そもそもそんなプラグインが作れるのかどうか調べてもいませんが)、ぜひとも作って公開していただければとも思います。