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 { 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: "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
 72            if output.stderr.starts_with("Everything up to date") {
 73                SuccessMessage {
 74                    message: output.stderr.trim().to_owned(),
 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            if output.stderr.contains("* [new branch]") {
123                let pr_hints = [
124                    // GitHub
125                    "Create a pull request",
126                    // Bitbucket
127                    "Create pull request",
128                    // GitLab
129                    "create a merge request",
130                ];
131                let style = if pr_hints
132                    .iter()
133                    .any(|indicator| output.stderr.contains(indicator))
134                {
135                    let finder = LinkFinder::new();
136                    let first_link = finder
137                        .links(&output.stderr)
138                        .filter(|link| *link.kind() == LinkKind::Url)
139                        .map(|link| link.start()..link.end())
140                        .next();
141                    if let Some(link) = first_link {
142                        let link = output.stderr[link].to_string();
143                        SuccessStyle::PushPrLink { link }
144                    } else {
145                        SuccessStyle::ToastWithLog { output }
146                    }
147                } else {
148                    SuccessStyle::ToastWithLog { output }
149                };
150                SuccessMessage {
151                    message: format!("Published {} to {}", branch_name, remote_ref.name),
152                    style,
153                }
154            } else if output.stderr.starts_with("Everything up to date") {
155                SuccessMessage {
156                    message: output.stderr.trim().to_owned(),
157                    style: SuccessStyle::Toast,
158                }
159            } else {
160                SuccessMessage {
161                    message: format!("Pushed {} to {}", branch_name, remote_ref.name),
162                    style: SuccessStyle::ToastWithLog { output },
163                }
164            }
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_push_new_branch_pull_request() {
175        let action = RemoteAction::Push(
176            SharedString::new("test_branch"),
177            Remote {
178                name: SharedString::new("test_remote"),
179            },
180        );
181
182        let output = RemoteCommandOutput {
183            stdout: String::new(),
184            stderr: String::from(
185                "
186                Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
187                remote:
188                remote: Create a pull request for 'test' on GitHub by visiting:
189                remote:      https://example.com/test/test/pull/new/test
190                remote:
191                To example.com:test/test.git
192                 * [new branch]      test -> test
193                ",
194            ),
195        };
196
197        let msg = format_output(&action, output);
198
199        if let SuccessStyle::PushPrLink { link } = &msg.style {
200            assert_eq!(link, "https://example.com/test/test/pull/new/test");
201        } else {
202            panic!("Expected PushPrLink variant");
203        }
204    }
205
206    #[test]
207    fn test_push_new_branch_merge_request() {
208        let action = RemoteAction::Push(
209            SharedString::new("test_branch"),
210            Remote {
211                name: SharedString::new("test_remote"),
212            },
213        );
214
215        let output = RemoteCommandOutput {
216            stdout: String::new(),
217            stderr: String::from("
218                Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
219                remote:
220                remote: To create a merge request for test, visit:
221                remote:   https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test
222                remote:
223                To example.com:test/test.git
224                 * [new branch]      test -> test
225                "),
226        };
227
228        let msg = format_output(&action, output);
229
230        if let SuccessStyle::PushPrLink { link } = &msg.style {
231            assert_eq!(
232                link,
233                "https://example.com/test/test/-/merge_requests/new?merge_request%5Bsource_branch%5D=test"
234            );
235        } else {
236            panic!("Expected PushPrLink variant");
237        }
238    }
239
240    #[test]
241    fn test_push_new_branch_no_link() {
242        let action = RemoteAction::Push(
243            SharedString::new("test_branch"),
244            Remote {
245                name: SharedString::new("test_remote"),
246            },
247        );
248
249        let output = RemoteCommandOutput {
250            stdout: String::new(),
251            stderr: String::from(
252                "
253                To http://example.com/test/test.git
254                 * [new branch]      test -> test
255                ",
256            ),
257        };
258
259        let msg = format_output(&action, output);
260
261        if let SuccessStyle::ToastWithLog { output } = &msg.style {
262            assert_eq!(
263                output.stderr,
264                "
265                To http://example.com/test/test.git
266                 * [new branch]      test -> test
267                "
268            );
269        } else {
270            panic!("Expected ToastWithLog variant");
271        }
272    }
273}