public async Task GitPullFromRepositoryAsync(string repositoryPath, string localDir, string branch, CancellationToken cancel = default)
    {
        string gitUrl = this.Settings.GitLabBaseUrl._CombineUrl(repositoryPath + ".git").ToString();

        gitUrl = gitUrl._ReplaceStr("https://", $"https://*****:*****@");

        await Lfs.CreateDirectoryAsync(localDir);

        // localDir ディレクトリに .git ディレクトリは存在するか?
        string dotgitDir = localDir._CombinePath(".git");

        bool init = false;

        StrDictionary <string> envVars = new StrDictionary <string>();

        // empty config
        string emptyCfgPath = Env.MyLocalTempDir._CombinePath("empty.txt");

        using (await Lock1.LockWithAwait(cancel))
        {
            if (await Lfs.IsFileExistsAsync(emptyCfgPath, cancel) == false)
            {
                await Lfs.WriteStringToFileAsync(emptyCfgPath, "\n\n", cancel : cancel);
            }
        }

        envVars.Add("GIT_CONFIG_GLOBAL", emptyCfgPath);
        envVars.Add("GIT_CONFIG_SYSTEM", emptyCfgPath);

        if (await Lfs.IsDirectoryExistsAsync(dotgitDir, cancel))
        {
            try
            {
                // update を試みる
                await EasyExec.ExecAsync(this.GitExe, $"pull origin {branch}", localDir, cancel : cancel, additionalEnvVars : envVars);
            }
            catch (Exception ex)
            {
                ex._Error();
                init = true;
            }
        }
        else
        {
            init = true;
        }

        if (init)
        {
            // 初期化する
            await Lfs.DeleteDirectoryAsync(localDir, true, cancel : cancel, forcefulUseInternalRecursiveDelete : true);

            // git clone をする
            await EasyExec.ExecAsync(this.GitExe, $"clone {gitUrl} {localDir}", cancel : cancel, additionalEnvVars : envVars);

            // update を試みる
            await EasyExec.ExecAsync(this.GitExe, $"pull origin {branch}", localDir, cancel : cancel, additionalEnvVars : envVars);
        }
    }
    // Git リポジトリの自動ダウンロード
    async Task Loop2_MainteUsersAsync(CancellationToken cancel = default)
    {
        long lastHookTick = -1;

        while (cancel.IsCancellationRequested == false)
        {
            try
            {
                await Lfs.CreateDirectoryAsync(this.Settings.GitMirrorDataRootDir);

                await Lfs.CreateDirectoryAsync(this.Settings.GitWebDataRootDir);
            }
            catch
            {
            }

            try
            {
                // Git リポジトリを列挙
                var projects = await this.GitLabClient.EnumProjectsAsync(cancel);

                ConcurrentHashSet <string> dirNames = new ConcurrentHashSet <string>(StrCmpi);

                // メモ: last_activity_at の値を信用してはならない。これは GitLab がキャッシュしているので、なかなか更新されない。

                var targetProjects = projects.Where(p => p.empty_repo == false && p.path_with_namespace._IsFilled() && p.default_branch._IsFilled() && p.visibility._IsDiffi("private")).OrderByDescending(x => x.last_activity_at).ThenBy(x => x.path_with_namespace, StrCmpi);

                await TaskUtil.ForEachAsync(8, targetProjects, async (proj, index, cancel) =>
                {
                    cancel.ThrowIfCancellationRequested();

                    try
                    {
                        string dirname = proj.GenerateDirName();

                        dirNames.Add(dirname);

                        string gitRoot = this.Settings.GitMirrorDataRootDir._CombinePath(dirname);
                        string webRoot = this.Settings.GitWebDataRootDir._CombinePath(dirname);

                        await this.GitLabClient.GitPullFromRepositoryAsync(proj.path_with_namespace !, gitRoot, proj.default_branch !, cancel);

                        await this.SyncGitLocalRepositoryDirToWebRootDirAsync(gitRoot, webRoot, cancel);
                    }
                    catch (Exception ex)
                    {
                        ex._Error();
                    }
                }, cancel);

                // GitLab 上に存在せず local に存在する gitRoot を列挙して削除する
                var existingLocalGitDirs = await Lfs.EnumDirectoryAsync(this.Settings.GitMirrorDataRootDir, cancel : cancel);

                foreach (var d in existingLocalGitDirs.Where(x => x.IsDirectory && dirNames.Contains(x.Name) == false))
                {
                    cancel.ThrowIfCancellationRequested();

                    try
                    {
                        await Lfs.DeleteDirectoryAsync(d.FullPath, true, cancel, true);
                    }
                    catch { }
                }

                // GitLab 上に存在せず local に存在する webRoot を列挙して削除する
                var existingLocalWebDirs = await Lfs.EnumDirectoryAsync(this.Settings.GitWebDataRootDir, cancel : cancel);

                foreach (var d in existingLocalWebDirs.Where(x => x.IsDirectory && dirNames.Contains(x.Name) == false))
                {
                    cancel.ThrowIfCancellationRequested();

                    try
                    {
                        await Lfs.DeleteFileAsync(d.FullPath._CombinePath(Consts.FileNames.LogBrowserSecureJson), cancel : cancel);
                    }
                    catch { }
                }
            }
            catch (Exception ex)
            {
                ex._Error();
            }

            await TaskUtil.AwaitWithPollAsync(this.Settings.ForceRepositoryUpdateIntervalMsecs, 500, () =>
            {
                long currentHookTick = this.HookFiredTick;

                if (lastHookTick != currentHookTick)
                {
                    lastHookTick = currentHookTick;
                    return(true);
                }

                return(false);
            },
                                              cancel,
                                              true);
        }
    }