1use std::fmt::Display;
4use std::fmt::Formatter;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use owo_colors::OwoColorize;
9use rootcause::prelude::ResultExt;
10use strum::EnumIter;
11use ytil_gh::RepoViewField;
12use ytil_gh::pr::IntoEnumIterator;
13use ytil_gh::pr::PullRequest;
14use ytil_gh::pr::PullRequestMergeState;
15use ytil_sys::pico_args::Arguments;
16
17pub fn run(mut pargs: Arguments) -> rootcause::Result<()> {
25 let search_filter: Option<String> = match pargs.opt_value_from_str("--search") {
26 Ok(search_filter) => search_filter,
27 Err(error) => {
28 eprintln!("{}", crate::cmds::Help::List.text());
29 return Err(error.into());
30 }
31 };
32 let merge_state = pargs
33 .opt_value_from_fn("--merge-state", PullRequestMergeState::from_str)
34 .attach_with(|| {
35 format!(
36 "accepted values are {:#?}",
37 PullRequestMergeState::iter().collect::<Vec<_>>()
38 )
39 });
40 let merge_state = match merge_state {
41 Ok(merge_state) => merge_state,
42 Err(error) => {
43 eprintln!("{}", crate::cmds::Help::List.text());
44 return Err(error.into());
45 }
46 };
47
48 ytil_gh::log_into_github()?;
49 let repo_name_with_owner = ytil_gh::get_repo_view_field(&RepoViewField::NameWithOwner)?;
50
51 let params = format!(
52 "search_filter={search_filter:?}{}",
53 merge_state
54 .map(|ms| format!("\nmerge_state={ms:?}"))
55 .unwrap_or_default()
56 );
57 println!("\n{}\n{}\n", "Search PRs by".cyan().bold(), params.white().bold());
58
59 let pull_requests = ytil_gh::pr::get(&repo_name_with_owner, search_filter.as_deref(), &|pr: &PullRequest| {
60 if let Some(merge_state) = merge_state {
61 return pr.merge_state == merge_state;
62 }
63 true
64 })?;
65
66 let renderable_prs: Vec<_> = pull_requests.into_iter().map(RenderablePullRequest).collect();
67 if renderable_prs.is_empty() {
68 println!("{}\n{}", "No matching PRs found".yellow().bold(), params.white().bold());
69 return Ok(());
70 }
71
72 let Some(selected_prs) = ytil_tui::minimal_multi_select(renderable_prs, ToString::to_string, ToString::to_string)?
73 else {
74 println!("No PRs selected");
75 return Ok(());
76 };
77
78 let Some(selected_op) = ytil_tui::minimal_select::<SelectableOp>(SelectableOp::iter().collect())? else {
79 println!("No operation selected");
80 return Ok(());
81 };
82
83 println!(); let selected_op_run = selected_op.run();
86 for pr in selected_prs.iter().map(Deref::deref) {
87 selected_op_run(pr);
88 }
89
90 Ok(())
91}
92
93pub struct RenderablePullRequest(pub PullRequest);
98
99impl Deref for RenderablePullRequest {
100 type Target = PullRequest;
101
102 fn deref(&self) -> &Self::Target {
103 &self.0
104 }
105}
106
107impl Display for RenderablePullRequest {
108 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109 write!(
111 f,
112 "{} {} ",
113 self.author.login.blue().bold(),
114 self.updated_at.strftime("%d-%m-%Y %H:%M UTC")
115 )?;
116 match self.merge_state {
117 PullRequestMergeState::Behind => write!(f, "{} ", "Behind".yellow().bold())?,
118 PullRequestMergeState::Blocked => write!(f, "{} ", "Blocked".red())?,
119 PullRequestMergeState::Clean => write!(f, "{} ", "Clean".green())?,
120 PullRequestMergeState::Dirty => write!(f, "{} ", "Dirty".red().bold())?,
121 PullRequestMergeState::Draft => write!(f, "{} ", "Draft".blue().bold())?,
122 PullRequestMergeState::HasHooks => write!(f, "{} ", "HasHooks".magenta())?,
123 PullRequestMergeState::Unknown => write!(f, "Unknown ")?,
124 PullRequestMergeState::Unmergeable => write!(f, "{} ", "Unmergeable".red().bold())?,
125 PullRequestMergeState::Unstable => write!(f, "{} ", "Unstable".magenta().bold())?,
126 }
127 write!(f, "{}", self.title)
128 }
129}
130
131#[derive(EnumIter)]
137enum SelectableOp {
138 Approve,
139 ApproveAndMerge,
140 DependabotRebase,
141 EnableAutoMerge,
142}
143
144impl Display for SelectableOp {
145 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 match self {
147 Self::Approve => write!(f, "{}", "Approve".green().bold()),
148 Self::ApproveAndMerge => write!(f, "{}", "Approve & Merge".green().bold()),
149 Self::DependabotRebase => write!(f, "{}", "Dependabot Rebase".blue().bold()),
150 Self::EnableAutoMerge => write!(f, "{}", "Enable auto-merge".magenta().bold()),
151 }
152 }
153}
154
155impl SelectableOp {
156 pub fn run(&self) -> Box<dyn Fn(&PullRequest)> {
157 match self {
158 Self::Approve => Box::new(|pr| {
159 drop(Op::Approve.report(pr, ytil_gh::pr::approve(pr.number)));
160 }),
161 Self::ApproveAndMerge => Box::new(|pr| {
162 drop(
163 Op::Approve
164 .report(pr, ytil_gh::pr::approve(pr.number))
165 .and_then(|()| Op::Merge.report(pr, ytil_gh::pr::merge(pr.number))),
166 );
167 }),
168 Self::DependabotRebase => Box::new(|pr| {
169 drop(Op::DependabotRebase.report(pr, ytil_gh::pr::dependabot_rebase(pr.number)));
170 }),
171 Self::EnableAutoMerge => Box::new(|pr| {
172 drop(Op::EnableAutoMerge.report(pr, ytil_gh::pr::enable_auto_merge(pr.number)));
173 }),
174 }
175 }
176}
177
178enum Op {
193 Approve,
194 Merge,
195 DependabotRebase,
196 EnableAutoMerge,
197}
198
199impl Op {
200 pub fn report(&self, pr: &PullRequest, res: rootcause::Result<()>) -> rootcause::Result<()> {
210 res.inspect(|()| self.report_ok(pr)).inspect_err(|err| {
211 self.report_error(pr, err);
212 })
213 }
214
215 fn report_ok(&self, pr: &PullRequest) {
217 let msg = match self {
218 Self::Approve => "Approved",
219 Self::Merge => "Merged",
220 Self::DependabotRebase => "Dependabot rebased",
221 Self::EnableAutoMerge => "Auto-merge enabled",
222 };
223 println!("{} {}", format!("{msg} PR").green().bold(), format_pr(pr));
224 }
225
226 fn report_error(&self, pr: &PullRequest, error: &rootcause::Report) {
228 let msg = match self {
229 Self::Approve => "approving",
230 Self::Merge => "merging",
231 Self::DependabotRebase => "triggering dependabot rebase",
232 Self::EnableAutoMerge => "enabling auto-merge",
233 };
234 eprintln!(
235 "{} {} error=\n{}",
236 format!("Error {msg} PR").red(),
237 format_pr(pr),
238 format!("{error:#?}").red()
239 );
240 }
241}
242
243fn format_pr(pr: &PullRequest) -> String {
245 format!(
246 "{}{:?} {}{:?} {}{:?}",
247 "number=".white().bold(),
248 pr.number,
249 "title=".white().bold(),
250 pr.title,
251 "author=",
252 pr.author,
253 )
254}