Skip to main content

silver_platter/
vcs.rs

1//! Version control system (VCS) support.
2use 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)]
11/// Errors that can occur when opening a branch.
12pub enum BranchOpenError {
13    /// The VCS is not supported.
14    Unsupported {
15        /// The URL of the branch.
16        url: url::Url,
17        /// A description of the error.
18        description: String,
19        /// The VCS that is not supported.
20        vcs: Option<String>,
21    },
22    /// The branch is missing.
23    Missing {
24        /// The URL of the branch.
25        url: url::Url,
26
27        /// A description of the error.
28        description: String,
29    },
30    /// The branch is rate limited.
31    RateLimited {
32        /// The URL of the branch.
33        url: url::Url,
34
35        /// A description of the error.
36        description: String,
37
38        /// The time to wait before retrying.
39        retry_after: Option<f64>,
40    },
41    /// The branch is unavailable.
42    Unavailable {
43        /// The URL of the branch.
44        url: url::Url,
45
46        /// A description of the error.
47        description: String,
48    },
49    /// The branch is temporarily unavailable.
50    TemporarilyUnavailable {
51        /// The URL of the branch.
52        url: url::Url,
53
54        /// A description of the error.
55        description: String,
56    },
57
58    /// An error occurred.
59    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    /// Convert a BrzError to a BranchOpenError.
101    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
236/// Open a branch from a URL.
237pub 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
265/// Open a branch, either at the specified URL or in a containing directory.
266///
267/// Return the branch and the subpath of the URL that was used to open it.
268pub 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
304/// Get the full URL for a branch.
305///
306/// Ideally this should just return Branch.user_url,
307/// but that currently exclude the branch name
308/// in some situations.
309pub 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        // Test Unsupported error
332        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        // Test Unsupported error with unknown VCS
343        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        // Test Missing error
354        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        // Test RateLimited error
364        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        // Test Unavailable error
375        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        // Test TemporarilyUnavailable error
385        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        // Test Other error
395        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        // Test NotBranchError conversion
402        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        // Test NotBranchError without reason
420        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        // Test ConnectionError with name resolution failure
434        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}