Files
mob/src/cmd/pr.cpp
T
isanae de4ad45f5b removed path_by_name()
added missing vs and qt paths
2020-12-03 17:45:31 -05:00

406 lines
7.9 KiB
C++

#include "pch.h"
#include "commands.h"
#include "../tasks/tasks.h"
namespace mob
{
std::string read_file(const fs::path& p)
{
std::ifstream t(p);
return {std::istreambuf_iterator<char>(t), std::istreambuf_iterator<char>()};
}
pr_command::pr_command()
: command(requires_options | handle_sigint)
{
}
command::meta_t pr_command::meta() const
{
return
{
"pr",
"applies changes from PRs"
};
}
std::string pr_command::do_doc()
{
return
"Operations:\n"
" - find: lists all the repos that would affected by `pull` or\n"
" `revert`\n"
" - pull: fetches the pr's branch and checks it out; all repos\n"
" will be in detached HEAD state\n"
" - revert: checks out branch `master` for every affected repo\n"
"\n"
"Repos that are not handled:\n"
" - mob itself\n"
" - umbrella\n"
" - any repo that's not in modorganizer_super\n"
" - modorganizer_installer";
}
clipp::group pr_command::do_group()
{
return
(clipp::command("pr")).set(picked_),
(clipp::option("-h", "--help") >> help_)
% ("shows this message"),
(clipp::option("--github-token")
& clipp::value("TOKEN") >> github_token_)
% "github api key",
(clipp::value("OP") >> op_)
% "one of `find`, `pull` or `revert`; see below",
(clipp::value("PR") >> pr_)
% "PR to apply, must be `task/pr`, such as `modorganizer/123`";
}
int pr_command::do_run()
{
if (github_token_.empty())
github_token_ = conf().global().get("github_key");
if (op_ == "pull")
return pull();
else if (op_ == "find")
return find();
else if (op_ == "revert")
return revert();
else
u8cerr << "bad operation '" << op_ << "'\n";
return 1;
}
std::pair<const tasks::modorganizer*, std::string> pr_command::parse_pr(
const std::string& pr) const
{
if (pr.empty())
return {};
const auto cs = split(pr, "/");
if (cs.size() != 2)
{
u8cerr << "--pr must be task/pr, such as modorganizer/123\n";
return {};
}
const std::string pattern = cs[0];
const std::string pr_number = cs[1];
const auto* task = find_one_task(pattern);
if (!task)
return {};
const auto* mo_task = dynamic_cast<const tasks::modorganizer*>(task);
if (!mo_task)
{
u8cerr << "only modorganizer tasks are supported\n";
return {};
}
return {mo_task, pr_number};
}
int pr_command::pull()
{
const auto prs = get_matching_prs(pr_);
if (prs.empty())
return 1;
const auto okay_prs = validate_prs(prs);
if (okay_prs.empty())
return 1;
try
{
for (auto&& pr : okay_prs)
{
const auto* task = dynamic_cast<const tasks::modorganizer*>(
find_one_task(pr.repo));
if (!task)
return 1;
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;
}
catch(std::exception& e)
{
u8cerr << e.what() << "\n";
return 1;
}
}
int pr_command::find()
{
return !get_matching_prs(pr_).empty();
}
int pr_command::revert()
{
const auto prs = get_matching_prs(pr_);
if (prs.empty())
return 1;
const auto okay_prs = validate_prs(prs);
if (okay_prs.empty())
return 1;
try
{
for (auto&& pr : okay_prs)
{
const auto* task = dynamic_cast<const tasks::modorganizer*>(
find_one_task(pr.repo));
if (!task)
return 1;
u8cout << "reverting " << task->name() << " to master\n";
git::checkout(task->this_source_path(), "master");
}
return 0;
}
catch(std::exception& e)
{
u8cerr << e.what() << "\n";
return 1;
}
}
std::vector<pr_command::pr_info> 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);
if (info.repo.empty())
return {};
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::pr_info> pr_command::search_prs(
const std::string& org, const std::string& author, const std::string& branch)
{
nlohmann::json json;
constexpr auto* pattern =
"https://api.github.com/search/issues?per_page=100&q="
"is:pr+org:{org:}+author:{author:}+is:open+head:{branch:}";
const auto search_url = fmt::format(
pattern,
fmt::arg("org", org),
fmt::arg("author", author),
fmt::arg("branch", branch));
u8cout << "search url is " << search_url << "\n";
u8cout << "searching for matching prs\n";
curl_downloader dl;
dl
.url(search_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<std::string, pr_info> 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<int>())
};
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 tasks::modorganizer* task, const std::string& pr)
{
nlohmann::json json;
if (github_token_.empty())
{
u8cerr << "missing --github-token\n";
return {};
}
const url u(fmt::format(
"https://api.github.com/repos/{}/{}/pulls/{}",
task->org(), task->repo(), pr));
curl_downloader dl;
dl
.url(u)
.header("Authorization", "token " + github_token_)
.start()
.join();
if (!dl.ok())
{
u8cerr << "failed to get pr info from github\n";
return {};
}
const auto output = dl.steal_output();
json = nlohmann::json::parse(output);
const std::string repo = json["head"]["repo"]["name"];
const std::string author = json["head"]["repo"]["owner"]["login"];
const std::string branch = json["head"]["ref"];
return {repo, author, branch};
}
std::vector<pr_command::pr_info> pr_command::validate_prs(
const std::vector<pr_info>& prs)
{
std::vector<std::string> problems;
std::vector<pr_info> okay_prs;
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<const tasks::modorganizer*>(tasks[0]);
if (!mo_task)
{
problems.push_back(
"task " + pr.repo + " is not a modorganizer repo");
continue;
}
}
}
okay_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 (okay_prs.empty())
{
u8cout << "all prs would be ignored, bailing out\n";
return {};
}
if (!ask_yes_no("these prs will be ignored; proceed anyway?", false))
return {};
u8cout << "\n";
}
return okay_prs;
}
} // namespace