mirror of
https://github.com/ModOrganizer2/mob.git
synced 2026-07-27 14:07:05 -07:00
finished implementing pr
This commit is contained in:
+12
-3
@@ -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<const modorganizer*, std::string> parse_pr(
|
||||
const std::string& pr) const;
|
||||
|
||||
pr_info get_pr_info(const modorganizer* task, const std::string& pr);
|
||||
|
||||
int pull_pr();
|
||||
std::vector<pr_command::pr_info> get_matching_prs(
|
||||
const std::string& repo_pr);
|
||||
|
||||
std::vector<pr_info> search_prs(
|
||||
const std::string& org,
|
||||
const std::string& author, const std::string& branch);
|
||||
|
||||
int pull();
|
||||
int find();
|
||||
};
|
||||
|
||||
|
||||
|
||||
+210
-31
@@ -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<const modorganizer*, std::string> 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<modorganizer*>(tasks[0]);
|
||||
const auto* task = find_one_task(pattern);
|
||||
if (!task)
|
||||
return {};
|
||||
|
||||
const auto* mo_task = dynamic_cast<const modorganizer*>(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<pr_info> checked_prs;
|
||||
|
||||
std::vector<std::string> 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<const modorganizer*>(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";
|
||||
for (auto&& pr : checked_prs)
|
||||
{
|
||||
const auto* task = dynamic_cast<const 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));
|
||||
task->git_url().string(),
|
||||
::fmt::format("pull/{}/head", pr.number));
|
||||
|
||||
u8cout << "checking out FETCH_HEAD\n";
|
||||
git::checkout(task->this_source_path(), "FETCH_HEAD");
|
||||
}
|
||||
|
||||
u8cout << "note: " << task->name() << " is in detached HEAD state\n";
|
||||
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::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);
|
||||
|
||||
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)
|
||||
{
|
||||
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<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 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
|
||||
|
||||
@@ -114,6 +114,33 @@ std::vector<task*> 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
|
||||
|
||||
@@ -24,6 +24,7 @@ Task& add_task(Args&&... args)
|
||||
void run_all_tasks();
|
||||
bool is_super_task(const std::string& name);
|
||||
std::vector<task*> find_tasks(const std::string& pattern);
|
||||
task* find_one_task(const std::string& pattern, bool verbose=true);
|
||||
|
||||
std::vector<task*> get_all_tasks();
|
||||
std::vector<task*> get_top_level_tasks();
|
||||
|
||||
+3
-3
@@ -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 <class F, class T>
|
||||
auto map(const std::vector<T>& v, F&& f)
|
||||
template <class Cont, class F>
|
||||
auto map(const Cont& v, F&& f)
|
||||
{
|
||||
using mapped_type = decltype(f(std::declval<T>()));
|
||||
using mapped_type = decltype(f(std::declval<Cont::value_type>()));
|
||||
std::vector<mapped_type> out;
|
||||
|
||||
for (auto&& e : v)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user