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}