debian_control/
vcs.rs

1//! Version Control System information
2use regex::Regex;
3use std::borrow::Cow;
4use std::str::FromStr;
5
6/// Parsed VCS information
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ParsedVcs {
9    /// URL of the repository
10    pub repo_url: String,
11
12    /// Name of the branch, if not the default branch
13    pub branch: Option<String>,
14
15    /// Subpath within the repository
16    pub subpath: Option<String>,
17}
18
19impl FromStr for ParsedVcs {
20    type Err = &'static str;
21
22    fn from_str(s: &str) -> Result<Self, Self::Err> {
23        let mut s: Cow<str> = s.trim().into();
24        let mut subpath: Option<String> = None;
25        let branch: Option<String>;
26        let repo_url: String;
27        let re = Regex::new(r" \[([^] ]+)\]").unwrap();
28
29        if let Some(ref m) = re.find(s.as_ref()) {
30            subpath = Some(m.as_str()[2..m.as_str().len() - 1].to_string());
31            s = Cow::Owned([s[..m.start()].to_string(), s[m.end()..].to_string()].concat());
32        }
33
34        if let Some(index) = s.find(" -b ") {
35            let (url, branch_str) = s.split_at(index);
36            branch = Some(branch_str[4..].to_string());
37            repo_url = url.to_string();
38        } else {
39            branch = None;
40            repo_url = s.to_string();
41        }
42
43        Ok(ParsedVcs {
44            repo_url,
45            branch,
46            subpath,
47        })
48    }
49}
50
51impl std::fmt::Display for ParsedVcs {
52    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
53        f.write_str(&self.repo_url)?;
54
55        if let Some(branch) = &self.branch {
56            write!(f, " -b {}", branch)?;
57        }
58
59        if let Some(subpath) = &self.subpath {
60            write!(f, " [{}]", subpath)?;
61        }
62
63        Ok(())
64    }
65}
66
67/// Version Control System information
68#[derive(Debug, Clone)]
69pub enum Vcs {
70    /// Git repository
71    Git {
72        /// URL of the repository
73        repo_url: String,
74
75        /// Name of the branch, if not the default branch
76        branch: Option<String>,
77
78        /// Subpath within the repository
79        subpath: Option<String>,
80    },
81    /// Bazaar branch
82    Bzr {
83        /// URL of the repository
84        repo_url: String,
85
86        /// Subpath within the repository
87        subpath: Option<String>,
88    },
89
90    /// Mercurial repository
91    Hg {
92        /// URL of the repository
93        repo_url: String,
94    },
95    /// Subversion repository
96    Svn {
97        /// URL of the repository, including branch path and subpath
98        url: String,
99    },
100    /// CVS repository
101    Cvs {
102        /// Root of the CVS repository
103        root: String,
104
105        /// Module within the CVS repository
106        module: Option<String>,
107    },
108}
109
110impl Vcs {
111    /// Parse a VCS field
112    ///
113    /// # Arguments
114    /// * `name` - Name of the VCS
115    /// * `value` - Value of the VCS field
116    pub fn from_field(name: &str, value: &str) -> Result<Vcs, String> {
117        match name {
118            "Git" => {
119                let parsed_vcs: ParsedVcs =
120                    value.parse::<ParsedVcs>().map_err(|e| e.to_string())?;
121                Ok(Vcs::Git {
122                    repo_url: parsed_vcs.repo_url,
123                    branch: parsed_vcs.branch,
124                    subpath: parsed_vcs.subpath,
125                })
126            }
127            "Bzr" => {
128                let parsed_vcs: ParsedVcs =
129                    value.parse::<ParsedVcs>().map_err(|e| e.to_string())?;
130                if parsed_vcs.branch.is_some() {
131                    return Err("Invalid branch value for Vcs-Bzr".to_string());
132                }
133                Ok(Vcs::Bzr {
134                    repo_url: parsed_vcs.repo_url,
135                    subpath: parsed_vcs.subpath,
136                })
137            }
138            "Hg" => Ok(Vcs::Hg {
139                repo_url: value.to_string(),
140            }),
141            "Svn" => Ok(Vcs::Svn {
142                url: value.to_string(),
143            }),
144            "Cvs" => {
145                if let Some((root, module)) = value.split_once(' ') {
146                    Ok(Vcs::Cvs {
147                        root: root.to_string(),
148                        module: Some(module.to_string()),
149                    })
150                } else {
151                    Ok(Vcs::Cvs {
152                        root: value.to_string(),
153                        module: None,
154                    })
155                }
156            }
157            n => Err(format!("Unknown VCS: {}", n)),
158        }
159    }
160
161    /// Convert the VCS information to a field
162    ///
163    /// Returns a tuple with the name of the VCS and the value of the field
164    pub fn to_field(&self) -> (&str, String) {
165        match self {
166            Vcs::Git {
167                repo_url,
168                branch,
169                subpath,
170            } => (
171                "Git",
172                ParsedVcs {
173                    repo_url: repo_url.to_string(),
174                    branch: branch.clone(),
175                    subpath: subpath.clone(),
176                }
177                .to_string(),
178            ),
179            Vcs::Bzr { repo_url, subpath } => (
180                "Bzr",
181                if let Some(subpath) = subpath {
182                    format!("{} [{}]", repo_url, subpath)
183                } else {
184                    repo_url.to_string()
185                },
186            ),
187            Vcs::Hg { repo_url } => ("Hg", repo_url.to_string()),
188            Vcs::Svn { url } => ("Svn", url.to_string()),
189            Vcs::Cvs { root, module } => ("Cvs", {
190                if let Some(module) = module {
191                    format!("{} {}", root, module)
192                } else {
193                    root.to_string()
194                }
195            }),
196        }
197    }
198
199    /// Extract the subpath from the VCS information
200    pub fn subpath(&self) -> Option<String> {
201        match self {
202            Vcs::Git { subpath, .. } => subpath.clone(),
203            Vcs::Bzr { subpath, .. } => subpath.clone(),
204            _ => None,
205        }
206    }
207
208    /// Convert the VCS information to a URL that is usable by Breezy
209    pub fn to_branch_url(&self) -> Option<String> {
210        match self {
211            Vcs::Git {
212                repo_url,
213                branch,
214                subpath: _,
215                // TODO: Proper URL encoding
216            } => Some(format!("{},branch={}", repo_url, branch.as_ref().unwrap())),
217            Vcs::Bzr {
218                repo_url,
219                subpath: _,
220            } => Some(repo_url.clone()),
221            Vcs::Hg { repo_url } => Some(repo_url.clone()),
222            Vcs::Svn { url } => Some(url.clone()),
223            _ => None,
224        }
225    }
226}
227
228#[cfg(test)]
229mod test {
230    use super::*;
231
232    #[test]
233    fn test_vcs_info() {
234        let vcs_info = ParsedVcs::from_str("https://github.com/jelmer/example").unwrap();
235        assert_eq!(vcs_info.repo_url, "https://github.com/jelmer/example");
236        assert_eq!(vcs_info.branch, None);
237        assert_eq!(vcs_info.subpath, None);
238    }
239
240    #[test]
241    fn test_vcs_info_with_branch() {
242        let vcs_info = ParsedVcs::from_str("https://github.com/jelmer/example -b branch").unwrap();
243        assert_eq!(vcs_info.repo_url, "https://github.com/jelmer/example");
244        assert_eq!(vcs_info.branch, Some("branch".to_string()));
245        assert_eq!(vcs_info.subpath, None);
246    }
247
248    #[test]
249    fn test_vcs_info_with_subpath() {
250        let vcs_info = ParsedVcs::from_str("https://github.com/jelmer/example [subpath]").unwrap();
251        assert_eq!(vcs_info.repo_url, "https://github.com/jelmer/example");
252        assert_eq!(vcs_info.branch, None);
253        assert_eq!(vcs_info.subpath, Some("subpath".to_string()));
254    }
255
256    #[test]
257    fn test_vcs_info_with_branch_and_subpath() {
258        let vcs_info =
259            ParsedVcs::from_str("https://github.com/jelmer/example -b branch [subpath]").unwrap();
260        assert_eq!(vcs_info.repo_url, "https://github.com/jelmer/example");
261        assert_eq!(vcs_info.branch, Some("branch".to_string()));
262        assert_eq!(vcs_info.subpath, Some("subpath".to_string()));
263    }
264
265    #[test]
266    fn test_eq() {
267        let vcs_info1 =
268            ParsedVcs::from_str("https://github.com/jelmer/example -b branch [subpath]").unwrap();
269        let vcs_info2 =
270            ParsedVcs::from_str("https://github.com/jelmer/example -b branch [subpath]").unwrap();
271        let vcs_info3 =
272            ParsedVcs::from_str("https://example.com/jelmer/example -b branch [subpath]").unwrap();
273
274        assert_eq!(vcs_info1, vcs_info2);
275        assert_ne!(vcs_info1, vcs_info3);
276    }
277}