1use breezyshim::branch::GenericBranch;
3use breezyshim::controldir::{open_containing_from_transport, open_from_transport};
4use breezyshim::error::Error as BrzError;
5use breezyshim::{
6 get_transport, join_segment_parameters, split_segment_parameters, Branch, Transport,
7};
8use percent_encoding::{utf8_percent_encode, CONTROLS};
9
10#[derive(Debug)]
11pub enum BranchOpenError {
13 Unsupported {
15 url: url::Url,
17 description: String,
19 vcs: Option<String>,
21 },
22 Missing {
24 url: url::Url,
26
27 description: String,
29 },
30 RateLimited {
32 url: url::Url,
34
35 description: String,
37
38 retry_after: Option<f64>,
40 },
41 Unavailable {
43 url: url::Url,
45
46 description: String,
48 },
49 TemporarilyUnavailable {
51 url: url::Url,
53
54 description: String,
56 },
57
58 Other(String),
60}
61
62impl std::fmt::Display for BranchOpenError {
63 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64 match self {
65 BranchOpenError::Unsupported {
66 url,
67 description,
68 vcs,
69 } => write!(
70 f,
71 "Unsupported VCS for {}: {} ({})",
72 url,
73 description,
74 vcs.as_deref().unwrap_or("unknown")
75 ),
76 BranchOpenError::Missing { url, description } => {
77 write!(f, "Missing branch {}: {}", url, description)
78 }
79 BranchOpenError::RateLimited {
80 url,
81 description,
82 retry_after,
83 } => write!(
84 f,
85 "Rate limited {}: {} (retry after: {:?})",
86 url, description, retry_after
87 ),
88 BranchOpenError::Unavailable { url, description } => {
89 write!(f, "Unavailable {}: {}", url, description)
90 }
91 BranchOpenError::TemporarilyUnavailable { url, description } => {
92 write!(f, "Temporarily unavailable {}: {}", url, description)
93 }
94 BranchOpenError::Other(e) => write!(f, "Error: {}", e),
95 }
96 }
97}
98
99impl BranchOpenError {
100 pub fn from_err(url: url::Url, e: &BrzError) -> Self {
102 match e {
103 BrzError::NotBranchError(e, reason) => {
104 let description = if let Some(reason) = reason {
105 format!("{}: {}", e, reason)
106 } else {
107 e.to_string()
108 };
109 Self::Missing { url, description }
110 }
111 BrzError::DependencyNotPresent(l, e) => Self::Unavailable {
112 url,
113 description: format!("missing {}: {}", l, e),
114 },
115 BrzError::NoColocatedBranchSupport => Self::Unsupported {
116 url,
117 description: "no colocated branch support".to_string(),
118 vcs: None,
119 },
120 BrzError::Socket(e) => Self::Unavailable {
121 url,
122 description: format!("Socket error: {}", e),
123 },
124 BrzError::UnsupportedProtocol(url, extra) => Self::Unsupported {
125 url: url.parse().unwrap(),
126 description: if let Some(extra) = extra {
127 format!("Unsupported protocol: {}", extra)
128 } else {
129 "Unsupported protocol".to_string()
130 },
131 vcs: None,
132 },
133 BrzError::ConnectionError(msg) => {
134 if e.to_string()
135 .contains("Temporary failure in name resolution")
136 {
137 Self::TemporarilyUnavailable {
138 url,
139 description: msg.to_string(),
140 }
141 } else {
142 Self::Unavailable {
143 url,
144 description: msg.to_string(),
145 }
146 }
147 }
148 BrzError::PermissionDenied(path, extra) => Self::Unavailable {
149 url,
150 description: format!(
151 "Permission denied: {}: {}",
152 path.to_string_lossy(),
153 extra.as_deref().unwrap_or("")
154 ),
155 },
156 BrzError::InvalidURL(url, extra) => Self::Unavailable {
157 url: url.parse().unwrap(),
158 description: extra
159 .as_ref()
160 .map(|s| s.to_string())
161 .unwrap_or_else(|| format!("Invalid URL: {}", url)),
162 },
163 BrzError::InvalidHttpResponse(_path, msg, _orig_error, headers) => {
164 if msg.to_string().contains("Unexpected HTTP status 429") {
165 if let Some(retry_after) = headers.get("Retry-After") {
166 match retry_after.parse::<f64>() {
167 Ok(retry_after) => {
168 return Self::RateLimited {
169 url,
170 description: e.to_string(),
171 retry_after: Some(retry_after),
172 };
173 }
174 Err(e) => {
175 log::warn!("Unable to parse retry-after header: {}", retry_after);
176 return Self::RateLimited {
177 url,
178 description: e.to_string(),
179 retry_after: None,
180 };
181 }
182 }
183 }
184 Self::RateLimited {
185 url,
186 description: e.to_string(),
187 retry_after: None,
188 }
189 } else {
190 Self::Unavailable {
191 url,
192 description: e.to_string(),
193 }
194 }
195 }
196 BrzError::TransportError(message) => Self::Unavailable {
197 url,
198 description: message.to_string(),
199 },
200 BrzError::UnusableRedirect(source, target, reason) => Self::Unavailable {
201 url,
202 description: format!("Unusable redirect: {} -> {}: {}", source, target, reason),
203 },
204 BrzError::UnsupportedVcs(vcs) => Self::Unsupported {
205 url,
206 description: e.to_string(),
207 vcs: Some(vcs.clone()),
208 },
209 BrzError::UnsupportedFormat(format) => Self::Unsupported {
210 url,
211 description: e.to_string(),
212 vcs: Some(format.clone()),
213 },
214 BrzError::UnknownFormat(_format) => Self::Unsupported {
215 url,
216 description: e.to_string(),
217 vcs: None,
218 },
219 BrzError::RemoteGitError(msg) => Self::Unavailable {
220 url,
221 description: msg.to_string(),
222 },
223 BrzError::LineEndingError(msg) => Self::Unavailable {
224 url,
225 description: msg.to_string(),
226 },
227 BrzError::IncompleteRead(_partial, _expected) => Self::Unavailable {
228 url,
229 description: e.to_string(),
230 },
231 _ => Self::Other(e.to_string()),
232 }
233 }
234}
235
236pub fn open_branch(
238 url: &url::Url,
239 possible_transports: Option<&mut Vec<Transport>>,
240 probers: Option<&[&dyn breezyshim::controldir::PyProber]>,
241 name: Option<&str>,
242) -> Result<GenericBranch, BranchOpenError> {
243 let (url, params) = split_segment_parameters(url);
244
245 let name_owned;
246 let name = if let Some(name) = name {
247 Some(name)
248 } else if let Some(param_name) = params.get("name") {
249 name_owned = param_name.clone();
250 Some(name_owned.as_str())
251 } else {
252 None
253 };
254
255 let transport = get_transport(&url, possible_transports)
256 .map_err(|e| BranchOpenError::from_err(url.clone(), &e))?;
257 let dir = open_from_transport(&transport, probers)
258 .map_err(|e| BranchOpenError::from_err(url.clone(), &e))?;
259
260 dir.open_branch(name)
261 .map(|branch| *branch)
262 .map_err(|e| BranchOpenError::from_err(url.clone(), &e))
263}
264
265pub fn open_branch_containing(
269 url: &url::Url,
270 possible_transports: Option<&mut Vec<Transport>>,
271 probers: Option<&[&dyn breezyshim::controldir::PyProber]>,
272 name: Option<&str>,
273) -> Result<(GenericBranch, String), BranchOpenError> {
274 let (url, params) = split_segment_parameters(url);
275
276 let name_owned;
277 let name = if let Some(name) = name {
278 Some(name)
279 } else if let Some(param_name) = params.get("name") {
280 name_owned = param_name.clone();
281 Some(name_owned.as_str())
282 } else {
283 None
284 };
285
286 let transport = match get_transport(&url, possible_transports) {
287 Ok(transport) => transport,
288 Err(e) => return Err(BranchOpenError::from_err(url.clone(), &e)),
289 };
290 let (dir, subpath) =
291 open_containing_from_transport(&transport, probers).map_err(|e| match e {
292 BrzError::UnknownFormat(_) => {
293 unreachable!("open_containing_from_transport should not return UnknownFormat")
294 }
295 e => BranchOpenError::from_err(url.clone(), &e),
296 })?;
297
298 let branch = dir
299 .open_branch(name)
300 .map_err(|e| BranchOpenError::from_err(url.clone(), &e))?;
301 Ok((*branch, subpath))
302}
303
304pub fn full_branch_url(branch: &dyn Branch) -> url::Url {
310 match branch.name() {
311 None => branch.get_user_url(),
312 Some(ref name) if name.is_empty() => branch.get_user_url(),
313 Some(name) => {
314 let (url, mut params) = split_segment_parameters(&branch.get_user_url());
315 params.insert(
316 "branch".to_string(),
317 utf8_percent_encode(&name, CONTROLS).to_string(),
318 );
319 join_segment_parameters(&url, params)
320 }
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use url::Url;
328
329 #[test]
330 fn test_branch_open_error_display() {
331 let err = BranchOpenError::Unsupported {
333 url: Url::parse("https://example.com/repo").unwrap(),
334 description: "Not supported".to_string(),
335 vcs: Some("git".to_string()),
336 };
337 assert_eq!(
338 err.to_string(),
339 "Unsupported VCS for https://example.com/repo: Not supported (git)"
340 );
341
342 let err = BranchOpenError::Unsupported {
344 url: Url::parse("https://example.com/repo").unwrap(),
345 description: "Not supported".to_string(),
346 vcs: None,
347 };
348 assert_eq!(
349 err.to_string(),
350 "Unsupported VCS for https://example.com/repo: Not supported (unknown)"
351 );
352
353 let err = BranchOpenError::Missing {
355 url: Url::parse("https://example.com/repo").unwrap(),
356 description: "Branch not found".to_string(),
357 };
358 assert_eq!(
359 err.to_string(),
360 "Missing branch https://example.com/repo: Branch not found"
361 );
362
363 let err = BranchOpenError::RateLimited {
365 url: Url::parse("https://example.com/repo").unwrap(),
366 description: "Too many requests".to_string(),
367 retry_after: Some(60.0),
368 };
369 assert_eq!(
370 err.to_string(),
371 "Rate limited https://example.com/repo: Too many requests (retry after: Some(60.0))"
372 );
373
374 let err = BranchOpenError::Unavailable {
376 url: Url::parse("https://example.com/repo").unwrap(),
377 description: "Server unavailable".to_string(),
378 };
379 assert_eq!(
380 err.to_string(),
381 "Unavailable https://example.com/repo: Server unavailable"
382 );
383
384 let err = BranchOpenError::TemporarilyUnavailable {
386 url: Url::parse("https://example.com/repo").unwrap(),
387 description: "Server maintenance".to_string(),
388 };
389 assert_eq!(
390 err.to_string(),
391 "Temporarily unavailable https://example.com/repo: Server maintenance"
392 );
393
394 let err = BranchOpenError::Other("Unknown error".to_string());
396 assert_eq!(err.to_string(), "Error: Unknown error");
397 }
398
399 #[test]
400 fn test_branch_open_error_from_err() {
401 let brz_err = BrzError::NotBranchError(
403 "Not a branch".to_string(),
404 Some("Additional info".to_string()),
405 );
406 let url = Url::parse("https://example.com/repo").unwrap();
407 let err = BranchOpenError::from_err(url.clone(), &brz_err);
408 match err {
409 BranchOpenError::Missing {
410 url: err_url,
411 description,
412 } => {
413 assert_eq!(err_url, url);
414 assert_eq!(description, "Not a branch: Additional info");
415 }
416 _ => panic!("Expected Missing error"),
417 }
418
419 let brz_err = BrzError::NotBranchError("Not a branch".to_string(), None);
421 let err = BranchOpenError::from_err(url.clone(), &brz_err);
422 match err {
423 BranchOpenError::Missing {
424 url: err_url,
425 description,
426 } => {
427 assert_eq!(err_url, url);
428 assert_eq!(description, "Not a branch");
429 }
430 _ => panic!("Expected Missing error"),
431 }
432
433 let brz_err = BrzError::ConnectionError("Temporary failure in name resolution".to_string());
435 let err = BranchOpenError::from_err(url.clone(), &brz_err);
436 match err {
437 BranchOpenError::TemporarilyUnavailable {
438 url: err_url,
439 description,
440 } => {
441 assert_eq!(err_url, url);
442 assert_eq!(description, "Temporary failure in name resolution");
443 }
444 _ => panic!("Expected TemporarilyUnavailable error"),
445 }
446 }
447}