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}