use std::sync::Arc;
use console::style;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Confirm;
use dialoguer::Editor;
use dialoguer::FuzzySelect;
use dialoguer::Input;
use crate::cmds::merge_request::MergeRequestBodyArgs;
use crate::cmds::project::Member;
use crate::config::ConfigProperties;
use crate::error;
use crate::Result;
#[derive(Builder)]
pub struct MergeRequestUserInput {
pub title: String,
pub description: String,
pub assignee: Member,
#[builder(default)]
pub reviewer: Member,
}
impl MergeRequestUserInput {
pub fn builder() -> MergeRequestUserInputBuilder {
MergeRequestUserInputBuilder::default()
}
pub fn new(title: &str, description: &str, user_id: i64, username: &str) -> Self {
MergeRequestUserInput {
title: title.to_string(),
description: description.to_string(),
assignee: Member::builder()
.id(user_id)
.username(username.to_string())
.build()
.unwrap(),
reviewer: Member::default(),
}
}
}
struct MemberSelector {
members: Vec<Member>,
}
impl MemberSelector {
pub fn new(members: Vec<Member>) -> Self {
Self { members }
}
pub fn prepare_assignee_list(
&self,
cli_assignee: Option<&Member>,
config_assignee: Option<Member>,
) -> Vec<Member> {
let mut selection_list = self.members.clone();
match (cli_assignee, config_assignee) {
(Some(cli), _) => {
selection_list.insert(0, cli.clone());
selection_list.insert(1, Member::default()); }
(None, Some(config)) => {
selection_list.insert(0, config);
selection_list.insert(1, Member::default()); }
(None, None) => {
selection_list.insert(0, Member::default());
}
}
selection_list
}
pub fn prepare_reviewer_list(
&self,
default_cli_reviewer: Option<&Member>,
assigned_member: &Member,
) -> Vec<Member> {
let mut selection_list = if default_cli_reviewer.is_some() {
vec![default_cli_reviewer.unwrap().clone(), Member::default()]
} else {
vec![Member::default()]
};
selection_list.extend(
self.members
.iter()
.filter(|m| m != &assigned_member)
.cloned(),
);
selection_list
}
}
pub fn prompt_user_merge_request_info(
default_title: &str,
default_description: &str,
default_cli_assignee: Option<&Member>,
default_cli_reviewer: Option<&Member>,
config: &Arc<dyn ConfigProperties>,
) -> Result<MergeRequestUserInput> {
let (title, description) = prompt_user_title_description(default_title, default_description);
let selector = MemberSelector::new(config.merge_request_members());
let assignee_list =
selector.prepare_assignee_list(default_cli_assignee, config.preferred_assignee_username());
let assignee_index = gather_member(&assignee_list, "Assignee:");
let assigned_member = assignee_list[assignee_index].clone();
let reviewer_list = selector.prepare_reviewer_list(default_cli_reviewer, &assigned_member);
let reviewer_index = gather_member(&reviewer_list, "Reviewer:");
Ok(MergeRequestUserInput::builder()
.title(title)
.description(description)
.assignee(assigned_member)
.reviewer(reviewer_list[reviewer_index].clone())
.build()
.unwrap())
}
fn gather_member(members: &[Member], prompt: &str) -> usize {
let usernames = members
.iter()
.map(|member| member.username.as_str())
.collect::<Vec<&str>>();
let assignee_selection_id = FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.default(0)
.items(&usernames)
.interact()
.unwrap();
if assignee_selection_id != 0 {
assignee_selection_id
} else {
0
}
}
pub fn prompt_user_title_description(
default_title: &str,
default_description: &str,
) -> (String, String) {
let title: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Title: ")
.default(default_title.to_string())
.interact_text()
.unwrap();
let description = get_description(default_description);
(title, description)
}
fn get_description(default_description: &str) -> String {
show_input("Description: ", default_description, true, Style::Bold);
let mut description = default_description.to_string();
let prompt = "Edit description";
while !confirm(prompt, false) {
description = if let Some(entry_msg) = Editor::new().edit(&description).unwrap() {
entry_msg
} else {
"".to_string()
};
show_input("Description: ", &description, true, Style::Bold);
}
description
}
pub enum Style {
Bold,
Light,
}
pub fn show_input(prompt: &str, data: &str, new_line: bool, font_style: Style) {
let mut prompt_style = style(prompt);
if let Style::Bold = font_style {
prompt_style = prompt_style.bold()
}
if new_line {
println!("{}", prompt_style);
println!("\n{}\n", data);
} else {
print!("{}: ", prompt_style);
println!("{}", data)
}
}
fn confirm(prompt: &str, default_answer: bool) -> bool {
if Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.default(default_answer)
.interact()
.unwrap()
{
return default_answer;
}
!default_answer
}
pub fn show_summary_merge_request(
commit_str: &str,
args: &MergeRequestBodyArgs,
accept: bool,
) -> Result<()> {
show_outgoing_changes_summary(commit_str);
show_input("Target branch", &args.target_branch, false, Style::Bold);
show_input("Assignee", &args.assignee.username, false, Style::Bold);
show_input("Reviewer", &args.reviewer.username, false, Style::Bold);
show_input("Title", &args.title, false, Style::Bold);
if !args.description.is_empty() {
show_input("Description:", &args.description, true, Style::Bold);
} else {
show_input("Description", "None", false, Style::Bold);
}
println!();
if accept || confirm("Confirm summary", true) {
Ok(())
} else {
Err(error::gen("User cancelled"))
}
}
pub fn show_outgoing_changes_summary(commit_str: &str) {
show_input(
"\nSummary of outgoing changes:",
commit_str,
true,
Style::Bold,
);
}
pub fn prompt_args() -> String {
Input::with_theme(&ColorfulTheme::default())
.with_prompt("args: ")
.allow_empty(true)
.interact_text()
.unwrap()
}
pub fn fuzzy_select(amps: Vec<String>) -> Result<String> {
let selection = dialoguer::FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("amp:")
.default(0)
.items(&s)
.interact()
.unwrap();
Ok(amps[selection].to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cmds::project::Member;
fn create_test_member(id: i64, username: &str) -> Member {
Member::builder()
.id(id)
.username(username.to_string())
.build()
.unwrap()
}
fn create_test_members() -> Vec<Member> {
vec![
create_test_member(1, "alice"),
create_test_member(2, "bob"),
create_test_member(3, "charlie"),
]
}
#[test]
fn test_prepare_assignee_list_with_cli_assignee() {
let members = create_test_members();
let original_members = members.clone();
let selector = MemberSelector::new(members);
let cli_assignee = create_test_member(4, "david");
let result = selector.prepare_assignee_list(
Some(&cli_assignee),
Some(create_test_member(5, "eve")), );
assert_eq!(result[0], cli_assignee); assert_eq!(result[1], Member::default()); assert_eq!(&result[2..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 2); }
#[test]
fn test_prepare_assignee_list_with_config_assignee() {
let members = create_test_members();
let original_members = members.clone();
let selector = MemberSelector::new(members);
let config_assignee = create_test_member(4, "eve");
let result = selector.prepare_assignee_list(None, Some(config_assignee.clone()));
assert_eq!(result[0], config_assignee); assert_eq!(result[1], Member::default()); assert_eq!(&result[2..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 2); }
#[test]
fn test_prepare_assignee_list_with_no_defaults() {
let members = create_test_members();
let original_members = members.clone();
let selector = MemberSelector::new(members);
let result = selector.prepare_assignee_list(None, None);
assert_eq!(result[0], Member::default()); assert_eq!(&result[1..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 1); }
#[test]
fn test_prepare_assignee_list_with_existing_member() {
let members = create_test_members();
let original_members = members.clone();
let selector = MemberSelector::new(members);
let cli_assignee = &original_members[0];
let result = selector.prepare_assignee_list(Some(cli_assignee), None);
assert_eq!(result[0], cli_assignee.clone()); assert_eq!(result[1], Member::default()); assert_eq!(&result[2..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 2); }
#[test]
fn test_prepare_reviewer_list_with_cli_reviewer_provided() {
let members = create_test_members();
let selector = MemberSelector::new(members);
let assignee = create_test_member(1, "alice");
let reviewer = create_test_member(2, "charlie");
let result = selector.prepare_reviewer_list(Some(&reviewer), &assignee);
assert_eq!(result[0], reviewer);
assert!(!result.contains(&assignee));
assert_eq!(result.len(), 4);
}
#[test]
fn test_prepare_reviewer_list_no_cli_reviewer_provided() {
let members = create_test_members();
let selector = MemberSelector::new(members);
let assignee = create_test_member(1, "alice");
let result = selector.prepare_reviewer_list(None::<&Member>, &assignee);
assert_eq!(result[0], Member::default());
assert!(!result.contains(&assignee));
assert_eq!(result.len(), 3); }
}