use crate::api_traits::{CommentMergeRequest, MergeRequest, RemoteProject, Timestamp};
use crate::cli::merge_request::MergeRequestOptions;
use crate::config::ConfigProperties;
use crate::display::{Column, DisplayBody};
use crate::error::{AddContext, GRError};
use crate::git::Repo;
use crate::io::{CmdInfo, ShellResponse, TaskRunner};
use crate::remote::{CacheCliArgs, CacheType, GetRemoteCliArgs, ListBodyArgs, ListRemoteCliArgs};
use crate::shell::BlockingCommand;
use crate::{dialog, display, exec, git, remote, Cmd, Result};
use std::fmt::{self, Display, Formatter};
use std::{
fs::File,
io::{BufRead, BufReader, Cursor, Write},
sync::Arc,
};
use super::common::{self, get_user};
use super::project::{Member, Project};
const GPT_PROMPT: &str = r#"
Act like a professional software engineer. You have finished a new feature and
want to create a new pull request for someone else to review your changes. You
will be provided by a list of commit messages with the following format:
- Newest commit is at the very bottom.
- The subject of the commit message is followed by a dash `-` and its short SHA.
- If there is a body, it will be immediately below the subject `-` short SHA line.
You will provide a description of these changes. The description must not
mention commit SHAs and it must not mention the number of commits included. Be
concise and provide at most two paragraphs describing the changes. Use
imperative mode, be short and to the point. The description for the pull
request will begin with the sentence `This merge request`.
The formatted output that you will provide is as follows:
- First line will be the title. Keep it within 80 characters width
- Next line is blank
- The following lines will be the description of the pull request
Below are the changes:"#;
#[derive(Builder, Clone, Debug, Default)]
#[builder(default)]
pub struct MergeRequestResponse {
pub id: i64,
pub web_url: String,
pub author: String,
pub updated_at: String,
pub source_branch: String,
pub sha: String,
pub created_at: String,
pub title: String,
pub pull_request: String,
pub description: String,
pub merged_at: String,
pub pipeline_id: Option<i64>,
pub pipeline_url: Option<String>,
}
impl MergeRequestResponse {
pub fn builder() -> MergeRequestResponseBuilder {
MergeRequestResponseBuilder::default()
}
}
impl From<MergeRequestResponse> for DisplayBody {
fn from(mr: MergeRequestResponse) -> DisplayBody {
DisplayBody {
columns: vec![
Column::new("ID", mr.id.to_string()),
Column::new("Title", mr.title),
Column::new("Source Branch", mr.source_branch),
Column::builder()
.name("SHA".to_string())
.value(mr.sha)
.optional(true)
.build()
.unwrap(),
Column::builder()
.name("Description".to_string())
.value(mr.description)
.optional(true)
.build()
.unwrap(),
Column::new("Author", mr.author),
Column::new("URL", mr.web_url),
Column::new("Updated at", mr.updated_at),
Column::builder()
.name("Merged at".to_string())
.value(mr.merged_at)
.optional(true)
.build()
.unwrap(),
Column::builder()
.name("Pipeline ID".to_string())
.value(mr.pipeline_id.map_or("".to_string(), |id| id.to_string()))
.optional(true)
.build()
.unwrap(),
Column::builder()
.name("Pipeline URL".to_string())
.value(mr.pipeline_url.unwrap_or("".to_string()))
.optional(true)
.build()
.unwrap(),
],
}
}
}
impl Timestamp for MergeRequestResponse {
fn created_at(&self) -> String {
self.created_at.clone()
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum MergeRequestState {
Opened,
Closed,
Merged,
}
impl TryFrom<&str> for MergeRequestState {
type Error = String;
fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
match s {
"opened" => Ok(MergeRequestState::Opened),
"closed" => Ok(MergeRequestState::Closed),
"merged" => Ok(MergeRequestState::Merged),
_ => Err(format!("Invalid merge request state: {}", s)),
}
}
}
impl Display for MergeRequestState {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
MergeRequestState::Opened => write!(f, "opened"),
MergeRequestState::Closed => write!(f, "closed"),
MergeRequestState::Merged => write!(f, "merged"),
}
}
}
#[derive(Builder, Debug)]
pub struct MergeRequestBodyArgs {
#[builder(default)]
pub title: String,
#[builder(default)]
pub description: String,
#[builder(default)]
pub source_branch: String,
#[builder(default)]
pub target_repo: String,
#[builder(default)]
pub target_branch: String,
#[builder(default)]
pub assignee: Member,
#[builder(default)]
pub reviewer: Member,
#[builder(default = "String::from(\"true\")")]
pub remove_source_branch: String,
#[builder(default)]
pub draft: bool,
#[builder(default)]
pub amend: bool,
}
impl MergeRequestBodyArgs {
pub fn builder() -> MergeRequestBodyArgsBuilder {
MergeRequestBodyArgsBuilder::default()
}
}
#[derive(Builder, Clone)]
pub struct MergeRequestListBodyArgs {
pub state: MergeRequestState,
pub list_args: Option<ListBodyArgs>,
#[builder(default)]
pub assignee: Option<Member>,
#[builder(default)]
pub author: Option<Member>,
#[builder(default)]
pub reviewer: Option<Member>,
}
impl MergeRequestListBodyArgs {
pub fn builder() -> MergeRequestListBodyArgsBuilder {
MergeRequestListBodyArgsBuilder::default()
}
}
#[derive(Builder, Clone)]
pub struct MergeRequestCliArgs {
pub title: Option<String>,
pub body_from_commit: Option<String>,
#[builder(default)]
pub body_from_file: Option<String>,
pub description: Option<String>,
pub description_from_file: Option<String>,
#[builder(default)]
pub assignee: Option<String>,
#[builder(default)]
pub reviewer: Option<String>,
#[builder(default)]
pub rand_reviewer: bool,
pub target_branch: Option<String>,
#[builder(default)]
pub target_repo: Option<String>,
#[builder(default)]
pub fetch: Option<String>,
#[builder(default)]
pub rebase: Option<String>,
pub auto: bool,
pub cache_args: CacheCliArgs,
pub open_browser: bool,
pub accept_summary: bool,
pub commit: Option<String>,
pub amend: bool,
pub force: bool,
pub draft: bool,
pub dry_run: bool,
#[builder(default)]
pub summary: SummaryOptions,
#[builder(default)]
pub patch: bool,
#[builder(default)]
pub gpt_prompt: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum SummaryOptions {
Short,
Long,
#[default]
None,
}
impl MergeRequestCliArgs {
pub fn builder() -> MergeRequestCliArgsBuilder {
MergeRequestCliArgsBuilder::default()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum MergeRequestUser {
Me,
Other(String),
}
#[derive(Builder)]
pub struct MergeRequestListCliArgs {
pub state: MergeRequestState,
pub list_args: ListRemoteCliArgs,
#[builder(default)]
pub assignee: Option<MergeRequestUser>,
#[builder(default)]
pub author: Option<MergeRequestUser>,
#[builder(default)]
pub reviewer: Option<MergeRequestUser>,
}
impl MergeRequestListCliArgs {
pub fn new(state: MergeRequestState, args: ListRemoteCliArgs) -> MergeRequestListCliArgs {
MergeRequestListCliArgs {
state,
list_args: args,
assignee: None,
author: None,
reviewer: None,
}
}
pub fn builder() -> MergeRequestListCliArgsBuilder {
MergeRequestListCliArgsBuilder::default()
}
}
#[derive(Builder)]
pub struct MergeRequestGetCliArgs {
pub id: i64,
pub get_args: GetRemoteCliArgs,
}
impl MergeRequestGetCliArgs {
pub fn builder() -> MergeRequestGetCliArgsBuilder {
MergeRequestGetCliArgsBuilder::default()
}
}
#[derive(Builder)]
pub struct CommentMergeRequestCliArgs {
pub id: i64,
pub comment: Option<String>,
pub comment_from_file: Option<String>,
}
impl CommentMergeRequestCliArgs {
pub fn builder() -> CommentMergeRequestCliArgsBuilder {
CommentMergeRequestCliArgsBuilder::default()
}
}
#[derive(Builder)]
pub struct CommentMergeRequestListCliArgs {
pub id: i64,
pub list_args: ListRemoteCliArgs,
}
impl CommentMergeRequestListCliArgs {
pub fn builder() -> CommentMergeRequestListCliArgsBuilder {
CommentMergeRequestListCliArgsBuilder::default()
}
}
#[derive(Builder)]
pub struct CommentMergeRequestListBodyArgs {
pub id: i64,
pub list_args: Option<ListBodyArgs>,
}
impl CommentMergeRequestListBodyArgs {
pub fn builder() -> CommentMergeRequestListBodyArgsBuilder {
CommentMergeRequestListBodyArgsBuilder::default()
}
}
#[derive(Builder)]
pub struct CommentMergeRequestBodyArgs {
pub id: i64,
pub comment: String,
}
impl CommentMergeRequestBodyArgs {
pub fn builder() -> CommentMergeRequestBodyArgsBuilder {
CommentMergeRequestBodyArgsBuilder::default()
}
}
#[derive(Builder, Clone)]
pub struct Comment {
pub id: i64,
pub body: String,
pub author: String,
pub created_at: String,
}
impl Comment {
pub fn builder() -> CommentBuilder {
CommentBuilder::default()
}
}
impl Timestamp for Comment {
fn created_at(&self) -> String {
self.created_at.clone()
}
}
impl From<Comment> for DisplayBody {
fn from(comment: Comment) -> Self {
DisplayBody::new(vec![
Column::new("ID", comment.id.to_string()),
Column::new("Body", comment.body),
Column::new("Author", comment.author),
Column::new("Created at", comment.created_at),
])
}
}
pub fn execute(
options: MergeRequestOptions,
config: Arc<dyn ConfigProperties>,
domain: String,
path: String,
) -> Result<()> {
match options {
MergeRequestOptions::Create(cli_args) => {
let mr_remote = remote::get_mr(
domain.clone(),
path.clone(),
config.clone(),
Some(&cli_args.cache_args),
CacheType::File,
)?;
let project_remote = remote::get_project(
domain,
path,
config.clone(),
Some(&cli_args.cache_args),
CacheType::File,
)?;
if let Some(commit_message) = &cli_args.commit {
git::add(&BlockingCommand)?;
git::commit(&BlockingCommand, commit_message)?;
}
let cmds = if let Some(description_file) = &cli_args.description_from_file {
let reader = get_reader_file_cli(description_file)?;
cmds(
project_remote,
&cli_args,
Arc::new(BlockingCommand),
Some(reader),
)
} else if let Some(body_from_file) = &cli_args.body_from_file {
let reader = get_reader_file_cli(body_from_file)?;
cmds(
project_remote,
&cli_args,
Arc::new(BlockingCommand),
Some(reader),
)
} else {
cmds(
project_remote,
&cli_args,
Arc::new(BlockingCommand),
None::<Cursor<&str>>,
)
};
let mr_body = get_repo_project_info(cmds)?;
if cli_args.summary != SummaryOptions::None {
return summary(mr_body, &cli_args);
}
if cli_args.patch {
return patch(mr_body, &cli_args);
}
open(mr_remote, config, mr_body, &cli_args)
}
MergeRequestOptions::List(cli_args) => list_merge_requests(domain, path, config, cli_args),
MergeRequestOptions::Merge { id } => {
let remote = remote::get_mr(domain, path, config, None, CacheType::None)?;
merge(remote, id)
}
MergeRequestOptions::Checkout { id } => {
let remote = remote::get_mr(domain, path, config, None, CacheType::File)?;
checkout(remote, id)
}
MergeRequestOptions::Close { id } => {
let remote = remote::get_mr(domain, path, config, None, CacheType::None)?;
close(remote, id)
}
MergeRequestOptions::CreateComment(cli_args) => {
let remote = remote::get_comment_mr(domain, path, config, None, CacheType::None)?;
if let Some(comment_file) = &cli_args.comment_from_file {
let reader = get_reader_file_cli(comment_file)?;
create_comment(remote, cli_args, Some(reader))
} else {
create_comment(remote, cli_args, None::<Cursor<&str>>)
}
}
MergeRequestOptions::ListComment(cli_args) => {
let remote = remote::get_comment_mr(
domain,
path,
config,
Some(&cli_args.list_args.get_args.cache_args),
CacheType::File,
)?;
let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
let body_args = CommentMergeRequestListBodyArgs::builder()
.id(cli_args.id)
.list_args(from_to_args)
.build()?;
if cli_args.list_args.num_pages {
return common::num_comment_merge_request_pages(
remote,
body_args,
std::io::stdout(),
);
}
if cli_args.list_args.num_resources {
return common::num_comment_merge_request_resources(
remote,
body_args,
std::io::stdout(),
);
}
list_comments(remote, body_args, cli_args, std::io::stdout())
}
MergeRequestOptions::Get(cli_args) => {
let remote = remote::get_mr(
domain,
path,
config,
Some(&cli_args.get_args.cache_args),
CacheType::File,
)?;
get_merge_request_details(remote, cli_args, std::io::stdout())
}
MergeRequestOptions::Approve { id } => {
let remote = remote::get_mr(domain, path, config, None, CacheType::None)?;
approve(remote, id, std::io::stdout())
}
}
}
pub fn get_reader_file_cli(file_path: &str) -> Result<Box<dyn BufRead + Send + Sync>> {
if file_path == "-" {
Ok(Box::new(BufReader::new(std::io::stdin())))
} else {
let file = File::open(file_path).err_context(GRError::PreconditionNotMet(format!(
"Cannot open file {}",
file_path
)))?;
Ok(Box::new(BufReader::new(file)))
}
}
fn get_filter_user(
user: &Option<MergeRequestUser>,
domain: &str,
path: &str,
config: &Arc<dyn ConfigProperties>,
list_args: &ListRemoteCliArgs,
) -> Result<Option<Member>> {
let member = match user {
Some(MergeRequestUser::Me) => Some(get_user(domain, path, config, list_args)?),
_ => None,
};
Ok(member)
}
pub fn list_merge_requests(
domain: String,
path: String,
config: Arc<dyn ConfigProperties>,
cli_args: MergeRequestListCliArgs,
) -> Result<()> {
let author = get_filter_user(
&cli_args.author,
&domain,
&path,
&config,
&cli_args.list_args,
)?;
let assignee = get_filter_user(
&cli_args.assignee,
&domain,
&path,
&config,
&cli_args.list_args,
)?;
let reviewer = get_filter_user(
&cli_args.reviewer,
&domain,
&path,
&config,
&cli_args.list_args,
)?;
let remote = remote::get_mr(
domain,
path,
config,
Some(&cli_args.list_args.get_args.cache_args),
CacheType::File,
)?;
let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
let body_args = MergeRequestListBodyArgs::builder()
.list_args(from_to_args)
.state(cli_args.state)
.assignee(assignee)
.author(author)
.reviewer(reviewer)
.build()?;
if cli_args.list_args.num_pages {
return common::num_merge_request_pages(remote, body_args, std::io::stdout());
}
if cli_args.list_args.num_resources {
return common::num_merge_request_resources(remote, body_args, std::io::stdout());
}
list(remote, body_args, cli_args, std::io::stdout())
}
fn get_member(members: &[Member], username: &str) -> Option<Member> {
members
.iter()
.find(|member| member.username == username)
.cloned()
}
fn user_prompt_confirmation(
mr_body: &MergeRequestBody,
config: Arc<dyn ConfigProperties>,
description: String,
target_branch: &String,
cli_args: &MergeRequestCliArgs,
) -> Result<MergeRequestBodyArgs> {
let mut title = mr_body.repo.title().to_string();
if cli_args.draft {
title = format!("DRAFT: {}", title);
}
let members = config.merge_request_members();
let assignee = if cli_args.assignee.is_some() {
get_member(&members, &cli_args.assignee.clone().unwrap())
} else {
None
};
let reviewer = if cli_args.reviewer.is_some() {
get_member(&members, &cli_args.reviewer.clone().unwrap())
} else if cli_args.rand_reviewer {
let members = config.merge_request_members();
let num_members = members.len();
if num_members == 0 {
None
} else {
let rand_index = rand::random::<usize>();
let rand_user = members[rand_index % num_members].clone();
Some(rand_user)
}
} else {
None
};
let user_input = if cli_args.auto {
let preferred_assignee_members =
[assignee.unwrap_or(config.preferred_assignee_username().unwrap_or_default())];
dialog::MergeRequestUserInput::builder()
.title(title)
.description(description)
.assignee(preferred_assignee_members[0].clone())
.reviewer(reviewer.unwrap_or_default())
.build()
.unwrap()
} else {
dialog::prompt_user_merge_request_info(
&title,
&description,
assignee.as_ref(),
reviewer.as_ref(),
&config,
)?
};
Ok(MergeRequestBodyArgs::builder()
.title(user_input.title)
.description(user_input.description)
.source_branch(mr_body.repo.current_branch().to_string())
.target_branch(target_branch.to_string())
.assignee(user_input.assignee)
.reviewer(user_input.reviewer)
.remove_source_branch("true".to_string())
.draft(cli_args.draft)
.amend(cli_args.amend)
.build()?)
}
fn open(
remote: Arc<dyn MergeRequest>,
config: Arc<dyn ConfigProperties>,
mr_body: MergeRequestBody,
cli_args: &MergeRequestCliArgs,
) -> Result<()> {
let source_branch = &mr_body.repo.current_branch();
let target_branch = cli_args.target_branch.clone();
let target_branch = target_branch.unwrap_or(mr_body.project.default_branch().to_string());
let description = build_description(
mr_body.repo.last_commit_message(),
config.merge_request_description_signature(),
);
in_feature_branch(source_branch, &target_branch)?;
let args = user_prompt_confirmation(&mr_body, config, description, &target_branch, cli_args)?;
if cli_args.rebase.is_some() {
git::rebase(&BlockingCommand, cli_args.rebase.as_ref().unwrap())?;
}
let outgoing_commits = git::outgoing_commits(
&BlockingCommand,
"origin",
&target_branch,
&SummaryOptions::Short,
)?;
if outgoing_commits.is_empty() {
return Err(GRError::PreconditionNotMet(
"No outgoing commits found. Please commit your changes.".to_string(),
)
.into());
}
if let Ok(()) =
dialog::show_summary_merge_request(&outgoing_commits, &args, cli_args.accept_summary)
{
println!("\nTaking off... 🚀\n");
if cli_args.dry_run {
println!("Dry run completed. No changes were made.");
return Ok(());
}
git::push(&BlockingCommand, "origin", &mr_body.repo, cli_args.force)?;
let merge_request_response = remote.open(args)?;
println!("Merge request opened: {}", merge_request_response.web_url);
if cli_args.open_browser {
open::that(merge_request_response.web_url)?;
}
}
Ok(())
}
fn summary(mr_body: MergeRequestBody, cli_args: &MergeRequestCliArgs) -> Result<()> {
let source_branch = mr_body.repo.current_branch();
let target_branch = cli_args.target_branch.clone();
let target_branch = target_branch.unwrap_or(mr_body.project.default_branch().to_string());
in_feature_branch(source_branch, &target_branch)?;
if cli_args.rebase.is_some() {
git::rebase(&BlockingCommand, cli_args.rebase.as_ref().unwrap())?;
}
let outgoing_commits = git::outgoing_commits(
&BlockingCommand,
"origin",
&target_branch,
&cli_args.summary,
)?;
if outgoing_commits.is_empty() {
return Err(GRError::PreconditionNotMet(
"No outgoing commits found. Please commit your changes.".to_string(),
)
.into());
}
if cli_args.gpt_prompt {
println!("{}", GPT_PROMPT);
}
println!("\n{}", outgoing_commits);
Ok(())
}
fn patch(mr_body: MergeRequestBody, cli_args: &MergeRequestCliArgs) -> Result<()> {
let source_branch = mr_body.repo.current_branch();
let target_branch = cli_args.target_branch.clone();
let target_branch = target_branch.unwrap_or(mr_body.project.default_branch().to_string());
in_feature_branch(source_branch, &target_branch)?;
if cli_args.rebase.is_some() {
git::rebase(&BlockingCommand, cli_args.rebase.as_ref().unwrap())?;
}
println!(
"{}",
git::patch(&BlockingCommand, source_branch, target_branch)?
);
Ok(())
}
fn cmds<R: BufRead + Send + Sync + 'static>(
remote: Arc<dyn RemoteProject + Send + Sync + 'static>,
cli_args: &MergeRequestCliArgs,
task_runner: Arc<impl TaskRunner<Response = ShellResponse> + Send + Sync + 'static>,
reader: Option<R>,
) -> Vec<Cmd<CmdInfo>> {
let remote_cl = remote.clone();
let remote_project_cmd = move || -> Result<CmdInfo> { remote_cl.get_project_data(None, None) };
let status_runner = task_runner.clone();
let git_status_cmd = || -> Result<CmdInfo> { git::status(status_runner) };
let current_branch_runner = task_runner.clone();
let git_current_branch = || -> Result<CmdInfo> { git::current_branch(current_branch_runner) };
let mut cmds: Vec<Cmd<CmdInfo>> = vec![
Box::new(remote_project_cmd),
Box::new(git_status_cmd),
Box::new(git_current_branch),
];
if cli_args.body_from_file.is_some() {
let reader = reader.unwrap();
let body_from_file_cmd = move || -> Result<CmdInfo> {
let mut description = String::new();
let mut lines = reader.lines();
let title = lines.next().unwrap_or_else(|| Ok("".to_string()))?;
lines.next();
for line in lines {
let line = line?;
description.push_str(&line);
description.push('\n');
}
Ok(CmdInfo::CommitBody(title, description))
};
cmds.push(Box::new(body_from_file_cmd));
} else {
let title = cli_args.title.clone();
let title = title.unwrap_or("".to_string());
let body_from_commit = cli_args.body_from_commit.clone();
let description_commit = cli_args.body_from_commit.clone();
let commit_summary_runner = task_runner.clone();
let git_title_cmd = move || -> Result<CmdInfo> {
if title.is_empty() {
git::commit_summary(commit_summary_runner, &body_from_commit)
} else {
Ok(CmdInfo::CommitSummary(title.clone()))
}
};
let description = cli_args.description.clone();
let description = description.unwrap_or("".to_string());
let commit_msg_runner = task_runner.clone();
let git_last_commit_message = move || -> Result<CmdInfo> {
if description.is_empty() {
if let Some(reader) = reader {
let mut description = String::new();
for line in reader.lines() {
let line = line?;
description.push_str(&line);
description.push('\n');
}
Ok(CmdInfo::CommitMessage(description))
} else {
git::commit_message(commit_msg_runner, &description_commit)
}
} else {
Ok(CmdInfo::CommitMessage(description.clone()))
}
};
cmds.push(Box::new(git_title_cmd));
cmds.push(Box::new(git_last_commit_message));
}
if cli_args.fetch.is_some() {
let fetch_runner = task_runner.clone();
let remote_alias = cli_args.fetch.as_ref().unwrap().clone();
let git_fetch_cmd = || -> Result<CmdInfo> { git::fetch(fetch_runner, remote_alias) };
cmds.push(Box::new(git_fetch_cmd));
}
cmds
}
fn build_description(description: &str, signature: &str) -> String {
if description.is_empty() && signature.is_empty() {
return "".to_string();
}
if description.is_empty() {
return signature.to_string();
}
if signature.is_empty() {
return description.to_string();
}
format!("{}\n\n{}", description, signature)
}
#[derive(Builder)]
struct MergeRequestBody {
repo: Repo,
project: Project,
}
impl MergeRequestBody {
fn builder() -> MergeRequestBodyBuilder {
MergeRequestBodyBuilder::default()
}
}
fn get_repo_project_info(cmds: Vec<Cmd<CmdInfo>>) -> Result<MergeRequestBody> {
let mut project = Project::default();
let mut repo = git::Repo::default();
let cmd_results = exec::parallel_stream(cmds);
for cmd_result in cmd_results {
match cmd_result {
Ok(CmdInfo::Project(project_data)) => {
project = project_data;
}
Ok(CmdInfo::StatusModified(status)) => repo.with_status(status),
Ok(CmdInfo::Branch(branch)) => repo.with_branch(&branch),
Ok(CmdInfo::CommitSummary(title)) => repo.with_title(&title),
Ok(CmdInfo::CommitMessage(message)) => repo.with_last_commit_message(&message),
Ok(CmdInfo::CommitBody(title, description)) => {
repo.with_title(&title);
repo.with_last_commit_message(&description);
}
Err(e) => return Err(e),
_ => {}
}
}
Ok(MergeRequestBody::builder()
.repo(repo)
.project(project)
.build()?)
}
fn in_feature_branch(current_branch: &str, upstream_branch: &str) -> Result<()> {
if current_branch == upstream_branch {
let trace = format!(
"Current branch {} is the same as the upstream \
remote {}. Please use a feature branch",
current_branch, upstream_branch
);
return Err(GRError::PreconditionNotMet(trace).into());
}
match current_branch {
"master" | "main" | "develop" => {
let trace = format!(
"Current branch is {}, which could be a release upstream branch. \
Please use a different feature branch name",
current_branch
);
Err(GRError::PreconditionNotMet(trace).into())
}
_ => Ok(()),
}
}
fn list<W: Write>(
remote: Arc<dyn MergeRequest>,
body_args: MergeRequestListBodyArgs,
cli_args: MergeRequestListCliArgs,
mut writer: W,
) -> Result<()> {
common::list_merge_requests(remote, body_args, cli_args, &mut writer)
}
fn merge(remote: Arc<dyn MergeRequest>, merge_request_id: i64) -> Result<()> {
let merge_request = remote.merge(merge_request_id)?;
println!("Merge request merged: {}", merge_request.web_url);
Ok(())
}
fn checkout(remote: Arc<dyn MergeRequest>, id: i64) -> Result<()> {
let merge_request = remote.get(id)?;
git::fetch(Arc::new(BlockingCommand), "origin".to_string())?;
git::checkout(&BlockingCommand, &merge_request.source_branch)
}
fn close(remote: Arc<dyn MergeRequest>, id: i64) -> Result<()> {
let merge_request = remote.close(id)?;
println!("Merge request closed: {}", merge_request.web_url);
Ok(())
}
fn approve<W: Write>(remote: Arc<dyn MergeRequest>, id: i64, mut writer: W) -> Result<()> {
let merge_request = remote.approve(id)?;
writer.write_all(format!("Merge request approved: {}\n", merge_request.web_url).as_bytes())?;
Ok(())
}
fn create_comment<R: BufRead>(
remote: Arc<dyn CommentMergeRequest>,
args: CommentMergeRequestCliArgs,
reader: Option<R>,
) -> Result<()> {
let comment = if let Some(comment) = args.comment {
comment
} else {
let mut comment = String::new();
reader.unwrap().read_to_string(&mut comment)?;
comment
};
remote.create(
CommentMergeRequestBodyArgs::builder()
.id(args.id)
.comment(comment)
.build()
.unwrap(),
)
}
pub fn get_merge_request_details<W: Write>(
remote: Arc<dyn MergeRequest>,
args: MergeRequestGetCliArgs,
mut writer: W,
) -> Result<()> {
let response = remote.get(args.id)?;
display::print(&mut writer, vec![response], args.get_args)?;
Ok(())
}
fn list_comments<W: Write>(
remote: Arc<dyn CommentMergeRequest>,
body_args: CommentMergeRequestListBodyArgs,
cli_args: CommentMergeRequestListCliArgs,
writer: W,
) -> Result<()> {
common::list_merge_request_comments(remote, body_args, cli_args, writer)
}
#[cfg(test)]
mod tests {
use std::{
io::{Cursor, Read},
sync::Mutex,
};
use crate::{
api_traits::CommentMergeRequest, cli::browse::BrowseOptions,
cmds::project::ProjectListBodyArgs, error,
};
use super::*;
#[test]
fn test_merge_request_args_with_custom_title() {
let args = MergeRequestBodyArgs::builder()
.source_branch("source".to_string())
.target_branch("target".to_string())
.title("title".to_string())
.build()
.unwrap();
assert_eq!(args.source_branch, "source");
assert_eq!(args.target_branch, "target");
assert_eq!(args.title, "title");
assert_eq!(args.remove_source_branch, "true");
assert_eq!(args.description, "");
}
#[test]
fn test_merge_request_get_all_fields() {
let assignee = Member::builder()
.id(1)
.username("username".to_string())
.build()
.unwrap();
let args = MergeRequestBodyArgs::builder()
.source_branch("source".to_string())
.target_branch("target".to_string())
.title("title".to_string())
.description("description".to_string())
.assignee(assignee)
.remove_source_branch("false".to_string())
.build()
.unwrap();
assert_eq!(args.source_branch, "source");
assert_eq!(args.target_branch, "target");
assert_eq!(args.title, "title");
assert_eq!(args.description, "description");
assert_eq!(args.assignee.id, 1);
assert_eq!(args.assignee.username, "username");
assert_eq!(args.remove_source_branch, "false");
}
#[test]
fn test_current_branch_should_not_be_the_upstream_branch() {
let current_branch = "current-branch";
let target_branch = "current-branch";
let result = in_feature_branch(current_branch, target_branch);
assert!(result.is_err());
}
#[test]
fn test_feature_branch_not_main_master_or_develop_is_ok() {
let current_branch = "newfeature";
let target_branch = "main";
let result = in_feature_branch(current_branch, target_branch);
assert!(result.is_ok());
}
#[test]
fn test_feature_branch_is_main_master_or_develop_should_err() {
let test_cases = [
("main", "upstream-branch"),
("master", "upstream-branch"),
("develop", "upstream-branch"),
];
for (current_branch, upstream_branch) in test_cases {
let result = in_feature_branch(current_branch, upstream_branch);
assert!(result.is_err());
}
}
fn get_cmds_mock(cmd: Arc<CmdMock>) -> Vec<Cmd<CmdInfo>> {
let cmd_status = cmd.clone();
let git_status_cmd =
move || -> Result<CmdInfo> { Ok(CmdInfo::StatusModified(cmd_status.status_modified)) };
let title_cmd = cmd.clone();
let git_title_cmd = move || -> Result<CmdInfo> {
Ok(CmdInfo::CommitSummary(
title_cmd.last_commit_summary.clone(),
))
};
let message_cmd = cmd.clone();
let git_message_cmd = move || -> Result<CmdInfo> {
Ok(CmdInfo::CommitMessage(
message_cmd.last_commit_message.clone(),
))
};
let branch_cmd = cmd.clone();
let git_current_branch =
move || -> Result<CmdInfo> { Ok(CmdInfo::Branch(branch_cmd.current_branch.clone())) };
let project_cmd = cmd.clone();
let remote_project_cmd =
move || -> Result<CmdInfo> { Ok(CmdInfo::Project(project_cmd.project.clone())) };
let members_cmd = cmd.clone();
let remote_members_cmd =
move || -> Result<CmdInfo> { Ok(CmdInfo::Members(members_cmd.members.clone())) };
let mut cmds: Vec<Cmd<CmdInfo>> = vec![
Box::new(remote_project_cmd),
Box::new(remote_members_cmd),
Box::new(git_status_cmd),
Box::new(git_title_cmd),
Box::new(git_message_cmd),
Box::new(git_current_branch),
];
if cmd.error {
let error_cmd =
move || -> Result<CmdInfo> { Err(error::gen("Failure retrieving data")) };
cmds.push(Box::new(error_cmd));
}
cmds
}
#[derive(Clone, Builder)]
struct CmdMock {
#[builder(default = "false")]
status_modified: bool,
last_commit_summary: String,
current_branch: String,
last_commit_message: String,
members: Vec<Member>,
project: Project,
#[builder(default = "false")]
error: bool,
}
#[test]
fn test_get_repo_project_info() {
let cmd_mock = CmdMockBuilder::default()
.status_modified(true)
.current_branch("current-branch".to_string())
.last_commit_summary("title".to_string())
.last_commit_message("last-commit-message".to_string())
.members(Vec::new())
.project(Project::default())
.build()
.unwrap();
let cmds = get_cmds_mock(Arc::new(cmd_mock));
let result = get_repo_project_info(cmds);
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result.repo.title(), "title");
assert_eq!(result.repo.current_branch(), "current-branch");
assert_eq!(result.repo.last_commit_message(), "last-commit-message");
}
#[test]
fn test_get_repo_project_info_cmds_error() {
let cmd_mock = CmdMockBuilder::default()
.status_modified(true)
.current_branch("current-branch".to_string())
.last_commit_summary("title".to_string())
.last_commit_message("last-commit-message".to_string())
.members(Vec::new())
.project(Project::default())
.error(true)
.build()
.unwrap();
let cmds = get_cmds_mock(Arc::new(cmd_mock));
let result = get_repo_project_info(cmds);
assert!(result.is_err());
}
#[test]
fn test_get_description_signature() {
let description_signature_table = [
("", "", ""),
("", "signature", "signature"),
("description", "", "description"),
("description", "signature", "description\n\nsignature"),
];
for (description, signature, expected) in description_signature_table {
let result = build_description(description, signature);
assert_eq!(result, expected);
}
}
#[test]
fn test_list_merge_requests() {
let remote = Arc::new(
MergeRequestRemoteMock::builder()
.merge_requests(vec![MergeRequestResponse::builder()
.id(1)
.title("New feature".to_string())
.web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
.author("author".to_string())
.updated_at("2021-01-01".to_string())
.build()
.unwrap()])
.build()
.unwrap(),
);
let mut buf = Vec::new();
let body_args = MergeRequestListBodyArgs::builder()
.list_args(None)
.state(MergeRequestState::Opened)
.assignee(None)
.build()
.unwrap();
let cli_args = MergeRequestListCliArgs::new(
MergeRequestState::Opened,
ListRemoteCliArgs::builder().build().unwrap(),
);
list(remote, body_args, cli_args, &mut buf).unwrap();
assert_eq!(
"ID|Title|Source Branch|Author|URL|Updated at\n\
1|New feature||author|https://gitlab.com/owner/repo/-/merge_requests/1|2021-01-01\n",
String::from_utf8(buf).unwrap(),
)
}
#[test]
fn test_if_no_merge_requests_are_available_list_should_return_no_merge_requests_found() {
let remote = Arc::new(MergeRequestRemoteMock::builder().build().unwrap());
let mut buf = Vec::new();
let body_args = MergeRequestListBodyArgs::builder()
.list_args(None)
.state(MergeRequestState::Opened)
.assignee(None)
.build()
.unwrap();
let cli_args = MergeRequestListCliArgs::new(
MergeRequestState::Opened,
ListRemoteCliArgs::builder().build().unwrap(),
);
list(remote, body_args, cli_args, &mut buf).unwrap();
assert_eq!("No resources found.\n", String::from_utf8(buf).unwrap(),)
}
#[test]
fn test_list_merge_requests_empty_with_flush_option_no_warn_message() {
let remote = Arc::new(MergeRequestRemoteMock::builder().build().unwrap());
let mut buf = Vec::new();
let body_args = MergeRequestListBodyArgs::builder()
.list_args(None)
.state(MergeRequestState::Opened)
.assignee(None)
.build()
.unwrap();
let cli_args = MergeRequestListCliArgs::new(
MergeRequestState::Opened,
ListRemoteCliArgs::builder().flush(true).build().unwrap(),
);
list(remote, body_args, cli_args, &mut buf).unwrap();
assert_eq!("", String::from_utf8(buf).unwrap());
}
#[test]
fn test_list_merge_requests_no_headers() {
let remote = Arc::new(
MergeRequestRemoteMock::builder()
.merge_requests(vec![MergeRequestResponse::builder()
.id(1)
.title("New feature".to_string())
.web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
.author("author".to_string())
.updated_at("2021-01-01".to_string())
.build()
.unwrap()])
.build()
.unwrap(),
);
let mut buf = Vec::new();
let body_args = MergeRequestListBodyArgs::builder()
.list_args(None)
.state(MergeRequestState::Opened)
.assignee(None)
.build()
.unwrap();
let cli_args = MergeRequestListCliArgs::new(
MergeRequestState::Opened,
ListRemoteCliArgs::builder()
.get_args(
GetRemoteCliArgs::builder()
.no_headers(true)
.build()
.unwrap(),
)
.build()
.unwrap(),
);
list(remote, body_args, cli_args, &mut buf).unwrap();
assert_eq!(
"1|New feature||author|https://gitlab.com/owner/repo/-/merge_requests/1|2021-01-01\n",
String::from_utf8(buf).unwrap(),
)
}
#[derive(Clone, Builder)]
struct MergeRequestRemoteMock {
#[builder(default = "Vec::new()")]
merge_requests: Vec<MergeRequestResponse>,
}
impl MergeRequestRemoteMock {
pub fn builder() -> MergeRequestRemoteMockBuilder {
MergeRequestRemoteMockBuilder::default()
}
}
impl MergeRequest for MergeRequestRemoteMock {
fn open(&self, _args: MergeRequestBodyArgs) -> Result<MergeRequestResponse> {
Ok(MergeRequestResponse::builder().build().unwrap())
}
fn list(&self, _args: MergeRequestListBodyArgs) -> Result<Vec<MergeRequestResponse>> {
Ok(self.merge_requests.clone())
}
fn merge(&self, _id: i64) -> Result<MergeRequestResponse> {
Ok(MergeRequestResponse::builder().build().unwrap())
}
fn get(&self, _id: i64) -> Result<MergeRequestResponse> {
Ok(self.merge_requests[0].clone())
}
fn close(&self, _id: i64) -> Result<MergeRequestResponse> {
Ok(MergeRequestResponse::builder().build().unwrap())
}
fn num_pages(&self, _args: MergeRequestListBodyArgs) -> Result<Option<u32>> {
Ok(None)
}
fn approve(&self, _id: i64) -> Result<MergeRequestResponse> {
Ok(self.merge_requests[0].clone())
}
fn num_resources(
&self,
_args: MergeRequestListBodyArgs,
) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
todo!()
}
}
#[derive(Default)]
struct MockRemoteProject {
comment_called: Mutex<bool>,
comment_argument: Mutex<String>,
list_comments: Vec<Comment>,
}
impl MockRemoteProject {
fn new(comments: Vec<Comment>) -> MockRemoteProject {
MockRemoteProject {
comment_called: Mutex::new(false),
comment_argument: Mutex::new("".to_string()),
list_comments: comments,
}
}
}
impl RemoteProject for MockRemoteProject {
fn get_project_data(&self, _id: Option<i64>, _path: Option<&str>) -> Result<CmdInfo> {
let project = Project::new(1, "main");
Ok(CmdInfo::Project(project))
}
fn get_project_members(&self) -> Result<CmdInfo> {
let members = vec![
Member::builder()
.id(1)
.username("user1".to_string())
.name("User 1".to_string())
.build()
.unwrap(),
Member::builder()
.id(2)
.username("user2".to_string())
.name("User 2".to_string())
.build()
.unwrap(),
];
Ok(CmdInfo::Members(members))
}
fn get_url(&self, _option: BrowseOptions) -> String {
todo!()
}
fn list(&self, _args: ProjectListBodyArgs) -> Result<Vec<Project>> {
todo!()
}
fn num_pages(&self, _args: ProjectListBodyArgs) -> Result<Option<u32>> {
todo!()
}
fn num_resources(
&self,
_args: ProjectListBodyArgs,
) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
todo!()
}
}
impl CommentMergeRequest for MockRemoteProject {
fn create(&self, args: CommentMergeRequestBodyArgs) -> Result<()> {
let mut called = self.comment_called.lock().unwrap();
*called = true;
let mut argument = self.comment_argument.lock().unwrap();
*argument = args.comment;
Ok(())
}
fn list(&self, _args: CommentMergeRequestListBodyArgs) -> Result<Vec<Comment>> {
Ok(self.list_comments.clone())
}
fn num_pages(&self, _args: CommentMergeRequestListBodyArgs) -> Result<Option<u32>> {
todo!()
}
fn num_resources(
&self,
_args: CommentMergeRequestListBodyArgs,
) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
todo!()
}
}
struct MockShellRunner {
responses: Mutex<Vec<ShellResponse>>,
}
impl MockShellRunner {
pub fn new(response: Vec<ShellResponse>) -> MockShellRunner {
MockShellRunner {
responses: Mutex::new(response),
}
}
}
impl TaskRunner for MockShellRunner {
type Response = ShellResponse;
fn run<T>(&self, _cmd: T) -> Result<Self::Response>
where
T: IntoIterator,
T::Item: AsRef<std::ffi::OsStr>,
{
let response = self.responses.lock().unwrap().pop().unwrap();
Ok(ShellResponse::builder()
.body(response.body)
.build()
.unwrap())
}
}
fn gen_cmd_responses() -> Vec<ShellResponse> {
let responses = vec![
ShellResponse::builder()
.body("fetch cmd".to_string())
.build()
.unwrap(),
ShellResponse::builder()
.body("last commit message cmd".to_string())
.build()
.unwrap(),
ShellResponse::builder()
.body("title git cmd".to_string())
.build()
.unwrap(),
ShellResponse::builder()
.body("current branch cmd".to_string())
.build()
.unwrap(),
ShellResponse::builder()
.body("status cmd".to_string())
.build()
.unwrap(),
];
responses
}
#[test]
fn test_cmds_gather_title_from_cli_arg() {
let remote = Arc::new(MockRemoteProject::default());
let cli_args = MergeRequestCliArgs::builder()
.title(Some("title cli".to_string()))
.body_from_commit(None)
.description(None)
.description_from_file(None)
.target_branch(Some("target-branch".to_string()))
.auto(false)
.cache_args(CacheCliArgs::default())
.open_browser(false)
.accept_summary(false)
.commit(Some("commit".to_string()))
.draft(false)
.force(false)
.amend(false)
.dry_run(false)
.build()
.unwrap();
let responses = gen_cmd_responses();
let task_runner = Arc::new(MockShellRunner::new(responses));
let cmds = cmds(remote, &cli_args, task_runner, None::<Cursor<&str>>);
assert_eq!(cmds.len(), 5);
let cmds = cmds
.into_iter()
.map(|cmd| cmd())
.collect::<Result<Vec<CmdInfo>>>()
.unwrap();
let title_result = cmds[3].clone();
let title = match title_result {
CmdInfo::CommitSummary(title) => title,
_ => "".to_string(),
};
assert_eq!("title cli", title);
}
#[test]
fn test_cmds_gather_title_from_git_commit_summary() {
let remote = Arc::new(MockRemoteProject::default());
let cli_args = MergeRequestCliArgs::builder()
.title(None)
.body_from_commit(None)
.description(None)
.description_from_file(None)
.target_branch(Some("target-branch".to_string()))
.auto(false)
.cache_args(CacheCliArgs::default())
.open_browser(false)
.accept_summary(false)
.commit(None)
.draft(false)
.force(false)
.amend(false)
.dry_run(false)
.build()
.unwrap();
let responses = gen_cmd_responses();
let task_runner = Arc::new(MockShellRunner::new(responses));
let cmds = cmds(remote, &cli_args, task_runner, None::<Cursor<&str>>);
let results = cmds
.into_iter()
.map(|cmd| cmd())
.collect::<Result<Vec<CmdInfo>>>()
.unwrap();
let title_result = results[3].clone();
let title = match title_result {
CmdInfo::CommitSummary(title) => title,
_ => "".to_string(),
};
assert_eq!("title git cmd", title);
}
#[test]
fn test_read_description_from_file() {
let remote = Arc::new(MockRemoteProject::default());
let cli_args = MergeRequestCliArgs::builder()
.title(None)
.body_from_commit(None)
.description(None)
.description_from_file(Some("description_file.txt".to_string()))
.target_branch(Some("target-branch".to_string()))
.auto(false)
.cache_args(CacheCliArgs::default())
.open_browser(false)
.accept_summary(false)
.commit(None)
.draft(false)
.force(false)
.amend(false)
.dry_run(false)
.build()
.unwrap();
let responses = gen_cmd_responses();
let task_runner = Arc::new(MockShellRunner::new(responses));
let description_contents = "This merge requests adds a new feature\n";
let reader = Cursor::new(description_contents);
let cmds = cmds(remote, &cli_args, task_runner, Some(reader));
let results = cmds
.into_iter()
.map(|cmd| cmd())
.collect::<Result<Vec<CmdInfo>>>()
.unwrap();
let description_result = results[4].clone();
let description = match description_result {
CmdInfo::CommitMessage(description) => description,
_ => "".to_string(),
};
assert_eq!(description_contents, description);
}
#[test]
fn test_create_comment_on_a_merge_request_with_cli_comment_ok() {
let remote = Arc::new(MockRemoteProject::default());
let cli_args = CommentMergeRequestCliArgs::builder()
.id(1)
.comment(Some("All features complete, ship it".to_string()))
.comment_from_file(None)
.build()
.unwrap();
let reader = Cursor::new("comment");
assert!(create_comment(remote.clone(), cli_args, Some(reader)).is_ok());
assert!(remote.comment_called.lock().unwrap().clone());
assert_eq!(
"All features complete, ship it",
remote.comment_argument.lock().unwrap().clone(),
);
}
#[test]
fn test_create_comment_on_a_merge_request_with_comment_from_file_ok() {
let remote = Arc::new(MockRemoteProject::default());
let cli_args = CommentMergeRequestCliArgs::builder()
.id(1)
.comment(None)
.comment_from_file(Some("comment_file.txt".to_string()))
.build()
.unwrap();
let reader = Cursor::new("Just a long, long comment from a file");
assert!(create_comment(remote.clone(), cli_args, Some(reader)).is_ok());
assert!(remote.comment_called.lock().unwrap().clone());
assert_eq!(
"Just a long, long comment from a file",
remote.comment_argument.lock().unwrap().clone(),
);
}
struct ErrorReader {}
impl Read for ErrorReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Error reading from reader",
))
}
}
impl BufRead for ErrorReader {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Error reading from reader",
))
}
fn consume(&mut self, _amt: usize) {}
}
#[test]
fn test_create_comment_on_a_merge_request_fail_to_read_comment_from_file() {
let remote = Arc::new(MockRemoteProject::default());
let cli_args = CommentMergeRequestCliArgs::builder()
.id(1)
.comment(None)
.comment_from_file(Some("comment_file.txt".to_string()))
.build()
.unwrap();
let reader = ErrorReader {};
assert!(create_comment(remote.clone(), cli_args, Some(reader)).is_err());
}
#[test]
fn test_get_merge_request_details() {
let cli_args = MergeRequestGetCliArgs::builder()
.id(1)
.get_args(
GetRemoteCliArgs::builder()
.display_optional(true)
.build()
.unwrap(),
)
.build()
.unwrap();
let response = MergeRequestResponse::builder()
.id(1)
.title("New feature".to_string())
.web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
.description("Implement get merge request".to_string())
.merged_at("2024-03-03T00:00:00Z".to_string())
.pipeline_id(Some(1))
.pipeline_url(Some(
"https://gitlab.com/owner/repo/-/pipelines/1".to_string(),
))
.build()
.unwrap();
let remote = Arc::new(
MergeRequestRemoteMock::builder()
.merge_requests(vec![response])
.build()
.unwrap(),
);
let mut writer = Vec::new();
get_merge_request_details(remote, cli_args, &mut writer).unwrap();
assert_eq!(
"ID|Title|Source Branch|SHA|Description|Author|URL|Updated at|Merged at|Pipeline ID|Pipeline URL\n\
1|New feature|||Implement get merge request||https://gitlab.com/owner/repo/-/merge_requests/1||2024-03-03T00:00:00Z|1|https://gitlab.com/owner/repo/-/pipelines/1\n",
String::from_utf8(writer).unwrap(),
)
}
#[test]
fn test_approve_merge_request_ok() {
let approve_response = MergeRequestResponse::builder()
.id(1)
.web_url("https://gitlab.com/owner/repo/-/merge_requests/1".to_string())
.build()
.unwrap();
let remote = Arc::new(
MergeRequestRemoteMock::builder()
.merge_requests(vec![approve_response])
.build()
.unwrap(),
);
let mut writer = Vec::new();
approve(remote, 1, &mut writer).unwrap();
assert_eq!(
"Merge request approved: https://gitlab.com/owner/repo/-/merge_requests/1\n",
String::from_utf8(writer).unwrap(),
);
}
#[test]
fn test_cmds_fetch_cli_arg() {
let remote = Arc::new(MockRemoteProject::default());
let cli_args = MergeRequestCliArgs::builder()
.title(Some("title cli".to_string()))
.body_from_commit(None)
.description(None)
.description_from_file(None)
.target_branch(Some("target-branch".to_string()))
.fetch(Some("origin".to_string()))
.auto(false)
.cache_args(CacheCliArgs::default())
.open_browser(false)
.accept_summary(false)
.commit(Some("commit".to_string()))
.draft(false)
.force(false)
.amend(false)
.dry_run(false)
.build()
.unwrap();
let responses = gen_cmd_responses();
let task_runner = Arc::new(MockShellRunner::new(responses));
let cmds = cmds(remote, &cli_args, task_runner, None::<Cursor<&str>>);
assert_eq!(cmds.len(), 6);
let cmds = cmds
.into_iter()
.map(|cmd| cmd())
.collect::<Result<Vec<CmdInfo>>>()
.unwrap();
let fetch_result = cmds[5].clone();
match fetch_result {
CmdInfo::Ignore => {}
_ => panic!("Expected ignore cmdinfo variant on fetch"),
};
}
#[test]
fn test_list_merge_request_comments() {
let comments = vec![
Comment::builder()
.id(1)
.body("Great work!".to_string())
.author("user1".to_string())
.created_at("2021-01-01".to_string())
.build()
.unwrap(),
Comment::builder()
.id(2)
.body("Keep it up!".to_string())
.author("user2".to_string())
.created_at("2021-01-02".to_string())
.build()
.unwrap(),
];
let remote = Arc::new(MockRemoteProject::new(comments));
let body_args = CommentMergeRequestListBodyArgs::builder()
.id(1)
.list_args(None)
.build()
.unwrap();
let cli_args = CommentMergeRequestListCliArgs::builder()
.id(1)
.list_args(ListRemoteCliArgs::builder().build().unwrap())
.build()
.unwrap();
let mut buf = Vec::new();
list_comments(remote, body_args, cli_args, &mut buf).unwrap();
assert_eq!(
"ID|Body|Author|Created at\n\
1|Great work!|user1|2021-01-01\n\
2|Keep it up!|user2|2021-01-02\n",
String::from_utf8(buf).unwrap(),
);
}
#[test]
fn test_gather_member_from_members_list() {
let members = vec![
Member::builder()
.id(1)
.username("user1".to_string())
.name("User 1".to_string())
.build()
.unwrap(),
Member::builder()
.id(2)
.username("user2".to_string())
.name("User 2".to_string())
.build()
.unwrap(),
];
let assignee_username = "user2";
let member = get_member(&members, assignee_username).unwrap();
assert_eq!(member.username, assignee_username);
assert_eq!(member.id, 2);
}
}