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}