remote_output.rs

  1use anyhow::Context as _;
  2
  3use git::repository::{Remote, RemoteCommandOutput};
  4use linkify::{LinkFinder, LinkKind};
  5use ui::SharedString;
  6use util::ResultExt as _;
  7
  8#[derive(Clone)]
  9pub enum RemoteAction {
 10    Fetch(Option<Remote>),
 11    Pull(Remote),
 12    Push(SharedString, Remote),
 13}
 14
 15impl RemoteAction {
 16    pub fn name(&self) -> &'static str {
 17        match self {
 18            RemoteAction::Fetch(_) => "fetch",
 19            RemoteAction::Pull(_) => "pull",
 20            RemoteAction::Push(_, _) => "push",
 21        }
 22    }
 23}
 24
 25pub enum SuccessStyle {
 26    Toast,
 27    ToastWithLog { output: RemoteCommandOutput },
 28    PushPrLink { text: String, link: String },
 29}
 30
 31pub struct SuccessMessage {
 32    pub message: String,
 33    pub style: SuccessStyle,
 34}
 35
 36pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
 37    match action {
 38        RemoteAction::Fetch(remote) => {
 39            if output.stderr.is_empty() {
 40                SuccessMessage {
 41                    message: "Fetch: Already up to date".into(),
 42                    style: SuccessStyle::Toast,
 43                }
 44            } else {
 45                let message = match remote {
 46                    Some(remote) => format!("Synchronized with {}", remote.name),
 47                    None => "Synchronized with remotes".into(),
 48                };
 49                SuccessMessage {
 50                    message,
 51                    style: SuccessStyle::ToastWithLog { output },
 52                }
 53            }
 54        }
 55        RemoteAction::Pull(remote_ref) => {
 56            let get_changes = |output: &RemoteCommandOutput| -> anyhow::Result<u32> {
 57                let last_line = output
 58                    .stdout
 59                    .lines()
 60                    .last()
 61                    .context("Failed to get last line of output")?
 62                    .trim();
 63
 64                let files_changed = last_line
 65                    .split_whitespace()
 66                    .next()
 67                    .context("Failed to get first word of last line")?
 68                    .parse()?;
 69
 70                Ok(files_changed)
 71            };
 72            if output.stdout.ends_with("Already up to date.\n") {
 73                SuccessMessage {
 74                    message: "Pull: Already up to date".into(),
 75                    style: SuccessStyle::Toast,
 76                }
 77            } else if output.stdout.starts_with("Updating") {
 78                let files_changed = get_changes(&output).log_err();
 79                let message = if let Some(files_changed) = files_changed {
 80                    format!(
 81                        "Received {} file change{} from {}",
 82                        files_changed,
 83                        if files_changed == 1 { "" } else { "s" },
 84                        remote_ref.name
 85                    )
 86                } else {
 87                    format!("Fast forwarded from {}", remote_ref.name)
 88                };
 89                SuccessMessage {
 90                    message,
 91                    style: SuccessStyle::ToastWithLog { output },
 92                }
 93            } else if output.stdout.starts_with("Merge") {
 94                let files_changed = get_changes(&output).log_err();
 95                let message = if let Some(files_changed) = files_changed {
 96                    format!(
 97                        "Merged {} file change{} from {}",
 98                        files_changed,
 99                        if files_changed == 1 { "" } else { "s" },
100                        remote_ref.name
101                    )
102                } else {
103                    format!("Merged from {}", remote_ref.name)
104                };
105                SuccessMessage {
106                    message,
107                    style: SuccessStyle::ToastWithLog { output },
108                }
109            } else if output.stdout.contains("Successfully rebased") {
110                SuccessMessage {
111                    message: format!("Successfully rebased from {}", remote_ref.name),
112                    style: SuccessStyle::ToastWithLog { output },
113                }
114            } else {
115                SuccessMessage {
116                    message: format!("Successfully pulled from {}", remote_ref.name),
117                    style: SuccessStyle::ToastWithLog { output },
118                }
119            }
120        }
121        RemoteAction::Push(branch_name, remote_ref) => {
122            let message = if output.stderr.ends_with("Everything up-to-date\n") {
123                "Push: Everything is up-to-date".to_string()
124            } else {
125                format!("Pushed {} to {}", branch_name, remote_ref.name)
126            };
127
128            let style = if output.stderr.ends_with("Everything up-to-date\n") {
129                Some(SuccessStyle::Toast)
130            } else if output.stderr.contains("\nremote: ") {
131                let pr_hints = [
132                    ("Create a pull request", "Create Pull Request"), // GitHub
133                    ("Create pull request", "Create Pull Request"),   // Bitbucket
134                    ("create a merge request", "Create Merge Request"), // GitLab
135                    ("View merge request", "View Merge Request"),     // GitLab
136                ];
137                pr_hints
138                    .iter()
139                    .find(|(indicator, _)| output.stderr.contains(indicator))
140                    .and_then(|(_, mapped)| {
141                        let finder = LinkFinder::new();
142                        finder
143                            .links(&output.stderr)
144                            .filter(|link| *link.kind() == LinkKind::Url)
145                            .map(|link| link.start()..link.end())
146                            .next()
147                            .map(|link| SuccessStyle::PushPrLink {
148                                text: mapped.to_string(),
149                                link: output.stderr[link].to_string(),
150                            })
151                    })
152            } else {
153                None
154            };
155            SuccessMessage {
156                message,
157                style: style.unwrap_or(SuccessStyle::ToastWithLog { output }),
158            }
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use indoc::indoc;
167
168    #[test]
169    fn test_push_new_branch_pull_request() {
170        let action = RemoteAction::Push(
171            SharedString::new("test_branch"),
172            Remote {
173                name: SharedString::new("test_remote"),
174            },
175        );
176
177        let output = RemoteCommandOutput {
178            stdout: String::new(),
179            stderr: indoc! { "
180                Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
181                remote:
182                remote: Create a pull request for 'test' on GitHub by visiting:
183                remote:      https://example.com/test/test/pull/new/test
184                remote:
185                To example.com:test/test.git
186                 * [new branch]      test -> test
187                "}
188            .to_string(),
189        };
190
191        let msg = format_output(&action, output);
192
193        if let SuccessStyle::PushPrLink { text: hint, link } = &msg.style {
194            assert_eq!(hint, "Create Pull Request");
195            assert_eq!(link, "https://example.com/test/test/pull/new/test");
196        } else {
197            panic!("Expected PushPrLink variant");
198        }
199    }
200
201    #[test]
202    fn test_push_new_branch_merge_request() {
203        let action = RemoteAction::Push(
204            SharedString::new("test_branch"),
205            Remote {
206                name: SharedString::new("test_remote"),
207            },
208        );
209
210        let output = RemoteCommandOutput {
211            stdout: String::new(),
212            stderr: indoc! {"
213                Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
214                remote:
215                remote: To create a merge request for test, visit:
216                remote:   https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test
217                remote:
218                To example.com:test/test.git
219                 * [new branch]      test -> test
220                "}
221            .to_string()
222            };
223
224        let msg = format_output(&action, output);
225
226        if let SuccessStyle::PushPrLink { text, link } = &msg.style {
227            assert_eq!(text, "Create Merge Request");
228            assert_eq!(
229                link,
230                "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test"
231            );
232        } else {
233            panic!("Expected PushPrLink variant");
234        }
235    }
236
237    #[test]
238    fn test_push_branch_existing_merge_request() {
239        let action = RemoteAction::Push(
240            SharedString::new("test_branch"),
241            Remote {
242                name: SharedString::new("test_remote"),
243            },
244        );
245
246        let output = RemoteCommandOutput {
247            stdout: String::new(),
248            stderr: indoc! {"
249                Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
250                remote:
251                remote: View merge request for test:
252                remote:    https://example.com/test/test/-/merge_requests/99999
253                remote:
254                To example.com:test/test.git
255                    + 80bd3c83be...e03d499d2e test -> test
256                "}
257            .to_string(),
258        };
259
260        let msg = format_output(&action, output);
261
262        if let SuccessStyle::PushPrLink { text, link } = &msg.style {
263            assert_eq!(text, "View Merge Request");
264            assert_eq!(link, "https://example.com/test/test/-/merge_requests/99999");
265        } else {
266            panic!("Expected PushPrLink variant");
267        }
268    }
269
270    #[test]
271    fn test_push_new_branch_no_link() {
272        let action = RemoteAction::Push(
273            SharedString::new("test_branch"),
274            Remote {
275                name: SharedString::new("test_remote"),
276            },
277        );
278
279        let output = RemoteCommandOutput {
280            stdout: String::new(),
281            stderr: indoc! { "
282                To http://example.com/test/test.git
283                 * [new branch]      test -> test
284                ",
285            }
286            .to_string(),
287        };
288
289        let msg = format_output(&action, output);
290
291        if let SuccessStyle::ToastWithLog { output } = &msg.style {
292            assert_eq!(
293                output.stderr,
294                "To http://example.com/test/test.git\n * [new branch]      test -> test\n"
295            );
296        } else {
297            panic!("Expected ToastWithLog variant");
298        }
299    }
300}