use anyhow::Context as _; use git::repository::{Remote, RemoteCommandOutput}; use linkify::{LinkFinder, LinkKind}; use ui::SharedString; use util::ResultExt as _; #[derive(Clone)] pub enum RemoteAction { Fetch(Option), Pull(Remote), Push(SharedString, Remote), } impl RemoteAction { pub fn name(&self) -> &'static str { match self { RemoteAction::Fetch(_) => "fetch", RemoteAction::Pull(_) => "pull", RemoteAction::Push(_, _) => "push", } } } pub enum SuccessStyle { Toast, ToastWithLog { output: RemoteCommandOutput }, PushPrLink { text: String, link: String }, } pub struct SuccessMessage { pub message: String, pub style: SuccessStyle, } pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage { match action { RemoteAction::Fetch(remote) => { if output.stderr.is_empty() { SuccessMessage { message: "Fetch: Already up to date".into(), style: SuccessStyle::Toast, } } else { let message = match remote { Some(remote) => format!("Synchronized with {}", remote.name), None => "Synchronized with remotes".into(), }; SuccessMessage { message, style: SuccessStyle::ToastWithLog { output }, } } } RemoteAction::Pull(remote_ref) => { let get_changes = |output: &RemoteCommandOutput| -> anyhow::Result { let last_line = output .stdout .lines() .last() .context("Failed to get last line of output")? .trim(); let files_changed = last_line .split_whitespace() .next() .context("Failed to get first word of last line")? .parse()?; Ok(files_changed) }; if output.stdout.ends_with("Already up to date.\n") { SuccessMessage { message: "Pull: Already up to date".into(), style: SuccessStyle::Toast, } } else if output.stdout.starts_with("Updating") { let files_changed = get_changes(&output).log_err(); let message = if let Some(files_changed) = files_changed { format!( "Received {} file change{} from {}", files_changed, if files_changed == 1 { "" } else { "s" }, remote_ref.name ) } else { format!("Fast forwarded from {}", remote_ref.name) }; SuccessMessage { message, style: SuccessStyle::ToastWithLog { output }, } } else if output.stdout.starts_with("Merge") { let files_changed = get_changes(&output).log_err(); let message = if let Some(files_changed) = files_changed { format!( "Merged {} file change{} from {}", files_changed, if files_changed == 1 { "" } else { "s" }, remote_ref.name ) } else { format!("Merged from {}", remote_ref.name) }; SuccessMessage { message, style: SuccessStyle::ToastWithLog { output }, } } else if output.stdout.contains("Successfully rebased") { SuccessMessage { message: format!("Successfully rebased from {}", remote_ref.name), style: SuccessStyle::ToastWithLog { output }, } } else { SuccessMessage { message: format!("Successfully pulled from {}", remote_ref.name), style: SuccessStyle::ToastWithLog { output }, } } } RemoteAction::Push(branch_name, remote_ref) => { let message = if output.stderr.ends_with("Everything up-to-date\n") { "Push: Everything is up-to-date".to_string() } else { format!("Pushed {} to {}", branch_name, remote_ref.name) }; let style = if output.stderr.ends_with("Everything up-to-date\n") { Some(SuccessStyle::Toast) } else if output.stderr.contains("\nremote: ") { let pr_hints = [ ("Create a pull request", "Create Pull Request"), // GitHub ("Create pull request", "Create Pull Request"), // Bitbucket ("create a merge request", "Create Merge Request"), // GitLab ("View merge request", "View Merge Request"), // GitLab ]; pr_hints .iter() .find(|(indicator, _)| output.stderr.contains(indicator)) .and_then(|(_, mapped)| { let finder = LinkFinder::new(); finder .links(&output.stderr) .filter(|link| *link.kind() == LinkKind::Url) .map(|link| link.start()..link.end()) .next() .map(|link| SuccessStyle::PushPrLink { text: mapped.to_string(), link: output.stderr[link].to_string(), }) }) } else { None }; SuccessMessage { message, style: style.unwrap_or(SuccessStyle::ToastWithLog { output }), } } } } #[cfg(test)] mod tests { use super::*; use indoc::indoc; #[test] fn test_push_new_branch_pull_request() { let action = RemoteAction::Push( SharedString::new("test_branch"), Remote { name: SharedString::new("test_remote"), }, ); let output = RemoteCommandOutput { stdout: String::new(), stderr: indoc! { " Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: Create a pull request for 'test' on GitHub by visiting: remote: https://example.com/test/test/pull/new/test remote: To example.com:test/test.git * [new branch] test -> test "} .to_string(), }; let msg = format_output(&action, output); if let SuccessStyle::PushPrLink { text: hint, link } = &msg.style { assert_eq!(hint, "Create Pull Request"); assert_eq!(link, "https://example.com/test/test/pull/new/test"); } else { panic!("Expected PushPrLink variant"); } } #[test] fn test_push_new_branch_merge_request() { let action = RemoteAction::Push( SharedString::new("test_branch"), Remote { name: SharedString::new("test_remote"), }, ); let output = RemoteCommandOutput { stdout: String::new(), stderr: indoc! {" Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: To create a merge request for test, visit: remote: https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test remote: To example.com:test/test.git * [new branch] test -> test "} .to_string() }; let msg = format_output(&action, output); if let SuccessStyle::PushPrLink { text, link } = &msg.style { assert_eq!(text, "Create Merge Request"); assert_eq!( link, "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test" ); } else { panic!("Expected PushPrLink variant"); } } #[test] fn test_push_branch_existing_merge_request() { let action = RemoteAction::Push( SharedString::new("test_branch"), Remote { name: SharedString::new("test_remote"), }, ); let output = RemoteCommandOutput { stdout: String::new(), stderr: indoc! {" Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: View merge request for test: remote: https://example.com/test/test/-/merge_requests/99999 remote: To example.com:test/test.git + 80bd3c83be...e03d499d2e test -> test "} .to_string(), }; let msg = format_output(&action, output); if let SuccessStyle::PushPrLink { text, link } = &msg.style { assert_eq!(text, "View Merge Request"); assert_eq!(link, "https://example.com/test/test/-/merge_requests/99999"); } else { panic!("Expected PushPrLink variant"); } } #[test] fn test_push_new_branch_no_link() { let action = RemoteAction::Push( SharedString::new("test_branch"), Remote { name: SharedString::new("test_remote"), }, ); let output = RemoteCommandOutput { stdout: String::new(), stderr: indoc! { " To http://example.com/test/test.git * [new branch] test -> test ", } .to_string(), }; let msg = format_output(&action, output); if let SuccessStyle::ToastWithLog { output } = &msg.style { assert_eq!( output.stderr, "To http://example.com/test/test.git\n * [new branch] test -> test\n" ); } else { panic!("Expected ToastWithLog variant"); } } }