diff --git a/src/cmd/commands.h b/src/cmd/commands.h index 8a23391..2c7615d 100644 --- a/src/cmd/commands.h +++ b/src/cmd/commands.h @@ -250,19 +250,28 @@ protected: private: struct pr_info { - std::string repo, org, branch; + std::string repo, author, branch, title, number; }; + + std::string op_; std::string pr_; std::string github_token_; - std::string method_; std::pair parse_pr( const std::string& pr) const; pr_info get_pr_info(const modorganizer* task, const std::string& pr); - int pull_pr(); + std::vector get_matching_prs( + const std::string& repo_pr); + + std::vector search_prs( + const std::string& org, + const std::string& author, const std::string& branch); + + int pull(); + int find(); }; diff --git a/src/cmd/pr.cpp b/src/cmd/pr.cpp index feb354a..8826701 100644 --- a/src/cmd/pr.cpp +++ b/src/cmd/pr.cpp @@ -13,7 +13,7 @@ std::string read_file(const fs::path& p) pr_command::pr_command() - : command(requires_options | handle_sigint), method_("apply") + : command(requires_options | handle_sigint) { } @@ -43,14 +43,21 @@ clipp::group pr_command::do_group() & clipp::value("TOKEN") >> github_token_) % "github api key", + (clipp::value("OP") >> op_) + % "one of `pull`, `find`", + (clipp::value("PR") >> pr_) % "PR to apply, must be `task/pr`, such as `modorganizer/123`"; } int pr_command::do_run() { - if (const auto r=pull_pr(); r != 0) - return r; + if (op_ == "pull") + return pull(); + else if (op_ == "find") + return find(); + else + u8cerr << "bad operation '" << op_ << "'\n"; return 1; } @@ -71,50 +78,108 @@ std::pair pr_command::parse_pr( const std::string pattern = cs[0]; const std::string pr_number = cs[1]; - const auto tasks = find_tasks(pattern); - - if (tasks.empty()) - { - u8cerr << "no task matches '" << pattern << "'\n"; - return {}; - } - else if (tasks.size() > 1) - { - u8cerr - << "found " << tasks.size() << " matches for pattern " - << "'" << pattern << "'\n" - << "the pattern must only match one task\n"; - - return {}; - } - - const auto* task = dynamic_cast(tasks[0]); + const auto* task = find_one_task(pattern); if (!task) + return {}; + + const auto* mo_task = dynamic_cast(task); + if (!mo_task) { u8cerr << "only modorganizer tasks are supported\n"; return {}; } - return {task, pr_number}; + return {mo_task, pr_number}; } -int pr_command::pull_pr() +int pr_command::pull() { - auto&& [task, pr] = parse_pr(pr_); - if (!task) + const auto prs = get_matching_prs(pr_); + if (prs.empty()) return 1; + std::vector checked_prs; + + std::vector problems; + + for (auto&& pr : prs) + { + if (pr.repo == "mob") + { + problems.push_back("there's a pr for mob itself"); + continue; + } + else + { + const auto tasks = find_tasks(pr.repo); + + if (tasks.empty()) + { + problems.push_back("task " + pr.repo + " does not exist"); + continue; + } + else if (tasks.size() > 1) + { + problems.push_back("found more than one task for repo " + pr.repo); + continue; + } + else + { + const auto* mo_task = dynamic_cast(tasks[0]); + + if (!mo_task) + { + problems.push_back( + "task " + pr.repo + " is not a modorganizer repo"); + + continue; + } + } + } + + checked_prs.push_back(pr); + } + + if (!problems.empty()) + { + { + console_color cc(console_color::yellow); + + u8cout << "\nproblems:\n"; + for (auto&& p : problems) + u8cout << " - " << p << "\n"; + } + + u8cout << "\n"; + if (!ask_yes_no("these prs will be ignored; proceed anyway?", false)) + return 1; + + u8cout << "\n"; + } + try { - u8cout << "fetching pr " << pr << " in " << task->name() << "\n"; - git::fetch( - task->this_source_path(), - task->git_url().string(), ::fmt::format("pull/{}/head", pr)); + for (auto&& pr : checked_prs) + { + const auto* task = dynamic_cast( + find_one_task(pr.repo)); - u8cout << "checking out FETCH_HEAD\n"; - git::checkout(task->this_source_path(), "FETCH_HEAD"); + if (!task) + return 1; - u8cout << "note: " << task->name() << " is in detached HEAD state\n"; + u8cout + << "checking out pr " << pr.number << " " + << "in " << task->name() << "\n"; + + git::fetch( + task->this_source_path(), + task->git_url().string(), + ::fmt::format("pull/{}/head", pr.number)); + + git::checkout(task->this_source_path(), "FETCH_HEAD"); + } + + u8cout << "note: all these repos are now in detached HEAD state\n"; return 0; } @@ -125,6 +190,120 @@ int pr_command::pull_pr() } } +int pr_command::find() +{ + return !get_matching_prs(pr_).empty(); +} + +std::vector pr_command::get_matching_prs( + const std::string& repo_pr) +{ + auto&& [task, src_pr] = parse_pr(repo_pr); + if (!task) + return {}; + + u8cout << "getting info for pr " << src_pr << " in " << task->name() << "\n"; + const auto info = get_pr_info(task, src_pr); + + u8cout << "found pr from " << info.author << ":" << info.branch << "\n"; + + u8cout << "searching\n"; + const auto prs = search_prs(task->org(), info.author, info.branch); + + u8cout << "found matching prs in " << prs.size() << " repos:\n"; + + u8cout + << table(map(prs, [&](auto&& pr) + { + return std::pair(pr.repo + "/" + pr.number, pr.title); + }), 2, 5) + << "\n"; + + return prs; +} + +std::vector pr_command::search_prs( + const std::string& org, const std::string& author, const std::string& branch) +{ + constexpr bool from_file = true; + + nlohmann::json json; + + if (from_file) + { + json = nlohmann::json::parse(read_file("c:\\tmp\\1277-search.json")); + if (json.empty()) + return {}; + } + else + { + constexpr auto* pattern = + "https://api.github.com/search/issues?q=" + "is:pr+org:{org:}+author:{author:}+is:open+head:{branch:}"; + + const auto url = ::fmt::format( + pattern, + ::fmt::arg("org", org), + ::fmt::arg("author", author), + ::fmt::arg("branch", branch)); + + u8cout << "search url is " << url << "\n"; + + u8cout << "searching for matching prs\n"; + + curl_downloader dl; + + dl + .url(url) + .header("Authorization", "token " + github_token_) + .start() + .join(); + + if (!dl.ok()) + { + u8cerr << "failed to search github\n"; + return {}; + } + + const auto output = dl.steal_output(); + json = nlohmann::json::parse(output); + } + + + std::map repos; + + for (auto&& item : json["items"]) + { + // ex: https://api.github.com/repos/ModOrganizer2/modorganizer-Installer + const std::string url = item["repository_url"]; + + const auto last_slash = url.find_last_of("/"); + if (last_slash == std::string::npos) + { + u8cerr << "bad repo url in search: '" << url << "'\n"; + return {}; + } + + const auto repo = url.substr(last_slash + 1); + + pr_info info = { + repo, author, branch, item["title"], + std::to_string(item["number"].get()) + }; + + if (!repos.emplace(repo, info).second) + { + u8cerr + << "multiple prs found in repo " << repo << ", " + << "not supported\n"; + + return {}; + } + } + + return map(repos, [&](auto&& pair){ return pair.second; }); +} + pr_command::pr_info pr_command::get_pr_info( const modorganizer* task, const std::string& pr) { @@ -169,10 +348,10 @@ pr_command::pr_info pr_command::get_pr_info( } const std::string repo = json["head"]["repo"]["name"]; - const std::string org = json["head"]["repo"]["owner"]["login"]; + const std::string author = json["head"]["repo"]["owner"]["login"]; const std::string branch = json["head"]["ref"]; - return {repo, org, branch}; + return {repo, author, branch}; } } // namespace diff --git a/src/tasks/task.cpp b/src/tasks/task.cpp index 90c9e1b..951b650 100644 --- a/src/tasks/task.cpp +++ b/src/tasks/task.cpp @@ -114,6 +114,33 @@ std::vector find_tasks(const std::string& pattern) return tasks; } +task* find_one_task(const std::string& pattern, bool verbose) +{ + const auto tasks = find_tasks(pattern); + + if (tasks.empty()) + { + if (verbose) + u8cerr << "no task matches '" << pattern << "'\n"; + + return nullptr; + } + else if (tasks.size() > 1) + { + if (verbose) + { + u8cerr + << "found " << tasks.size() << " matches for pattern " + << "'" << pattern << "'\n" + << "the pattern must only match one task\n"; + } + + return nullptr; + } + + return tasks[0]; +} + void run_all_tasks() { try diff --git a/src/tasks/task.h b/src/tasks/task.h index 788056c..a6b60ac 100644 --- a/src/tasks/task.h +++ b/src/tasks/task.h @@ -24,6 +24,7 @@ Task& add_task(Args&&... args) void run_all_tasks(); bool is_super_task(const std::string& name); std::vector find_tasks(const std::string& pattern); +task* find_one_task(const std::string& pattern, bool verbose=true); std::vector get_all_tasks(); std::vector get_top_level_tasks(); diff --git a/src/utility/algo.h b/src/utility/algo.h index 5e9e978..0e778af 100644 --- a/src/utility/algo.h +++ b/src/utility/algo.h @@ -142,10 +142,10 @@ Container zip(const Range1& range1, const Range2& range2) // returns a vector containing the result of `f(e)` for each element `e` of `v` // -template -auto map(const std::vector& v, F&& f) +template +auto map(const Cont& v, F&& f) { - using mapped_type = decltype(f(std::declval())); + using mapped_type = decltype(f(std::declval())); std::vector out; for (auto&& e : v) diff --git a/src/utility/io.cpp b/src/utility/io.cpp index 0db9c1b..dd9304c 100644 --- a/src/utility/io.cpp +++ b/src/utility/io.cpp @@ -81,6 +81,26 @@ std::mutex& global_output_mutex() return g_output_mutex; } +bool ask_yes_no(const std::string& text, bool def) +{ + u8cout + << text + << (text.empty() ? "" : " ") + << (def ? "[Y/n]" : "[y/N]") + << " "; + + // stdin is not utf8 + std::string line; + std::getline(std::cin, line); + + if (line.empty()) + return def; + else if (line == "y" || line == "Y") + return true; + else + return false; +} + void u8stream::do_output(const std::string& s) { diff --git a/src/utility/io.h b/src/utility/io.h index feb01df..c3c6b80 100644 --- a/src/utility/io.h +++ b/src/utility/io.h @@ -102,6 +102,10 @@ void set_std_streams(); // std::mutex& global_output_mutex(); +// asks the user for y/n +// +bool ask_yes_no(const std::string& text, bool def); + // see https://github.com/isanae/mob/issues/4 //