debian_control/
vcs.rs

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