remote_output.rs

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