debian_analyzer/
vcs.rs

1//! Information about version control systems.
2use debian_control::vcs::ParsedVcs;
3use log::debug;
4use url::Url;
5
6/// List of known GitLab sites.
7pub const KNOWN_GITLAB_SITES: &[&str] = &["salsa.debian.org", "invent.kde.org", "0xacab.org"];
8
9/// Check if a particular host is a GitLab instance.
10pub fn is_gitlab_site(hostname: &str, net_access: Option<bool>) -> bool {
11    if KNOWN_GITLAB_SITES.contains(&hostname) {
12        return true;
13    }
14
15    if hostname.starts_with("gitlab.") {
16        return true;
17    }
18
19    if net_access.unwrap_or(false) {
20        probe_gitlab_host(hostname)
21    } else {
22        false
23    }
24}
25
26/// Check if a particular host is a GitLab instance.
27pub fn probe_gitlab_host(hostname: &str) -> bool {
28    use reqwest::header::HeaderMap;
29    let url = format!("https://{}/api/v4/version", hostname);
30
31    let mut headers = HeaderMap::new();
32    headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap());
33
34    let client = reqwest::blocking::Client::builder()
35        .default_headers(headers)
36        .build()
37        .expect("Failed to build HTTP client");
38
39    let http_url: reqwest::Url = url.parse().expect("Invalid URL format");
40
41    let request = client
42        .get(http_url)
43        .build()
44        .expect("Failed to build request");
45
46    let response = match client.execute(request) {
47        Ok(r) => r,
48        Err(_) => return false,
49    };
50
51    match response.status().as_u16() {
52        401 => {
53            if let Ok(data) = response.json::<serde_json::Value>() {
54                if let Some(message) = data["message"].as_str() {
55                    if message == "401 Unauthorized" {
56                        true
57                    } else {
58                        debug!("failed to parse JSON response: {:?}", data);
59                        false
60                    }
61                } else {
62                    debug!("failed to parse JSON response: {:?}", data);
63                    false
64                }
65            } else {
66                debug!("failed to parse JSON response");
67                false
68            }
69        }
70        200 => true,
71        _ => {
72            debug!("unexpected HTTP status code: {:?}", response.status());
73            false
74        }
75    }
76}
77
78/// Determine the URL of the browser for a GitLab repository.
79pub fn determine_gitlab_browser_url(url: &str) -> Url {
80    let parsed_vcs: ParsedVcs = url.trim_end_matches('/').parse().unwrap();
81
82    // TODO(jelmer): Add support for branches
83    let parsed_url = Url::parse(&parsed_vcs.repo_url).unwrap();
84
85    let path = parsed_url
86        .path()
87        .trim_end_matches('/')
88        .trim_end_matches(".git");
89
90    let branch = if let Some(branch) = parsed_vcs.branch {
91        Some(branch)
92    } else if parsed_vcs.subpath.is_some() {
93        Some("HEAD".to_string())
94    } else {
95        None
96    };
97
98    let mut path = if let Some(branch) = branch {
99        format!("{}/-/tree/{}", path, branch)
100    } else {
101        path.to_string()
102    };
103
104    if let Some(subpath) = parsed_vcs.subpath {
105        path.push_str(&format!("/{}", subpath));
106    }
107
108    let url = format!(
109        "https://{}/{}",
110        parsed_url.host_str().unwrap(),
111        path.trim_start_matches('/')
112    );
113
114    Url::parse(&url).unwrap()
115}
116
117/// Determine the URL of the browser for a VCS repository.
118pub fn determine_browser_url(
119    _vcs_type: &str,
120    vcs_url: &str,
121    net_access: Option<bool>,
122) -> Option<Url> {
123    let parsed_vcs: ParsedVcs = vcs_url.parse().unwrap();
124
125    let parsed_url: Url = parsed_vcs.repo_url.parse().unwrap();
126
127    match parsed_url.host_str().unwrap() {
128        host if is_gitlab_site(host, net_access) => Some(determine_gitlab_browser_url(vcs_url)),
129
130        "github.com" => {
131            let path = parsed_url.path().trim_end_matches(".git");
132
133            let branch = if let Some(branch) = parsed_vcs.branch {
134                Some(branch)
135            } else if parsed_vcs.subpath.is_some() {
136                Some("HEAD".to_string())
137            } else {
138                None
139            };
140
141            let mut path = if let Some(branch) = branch {
142                format!("{}/tree/{}", path, branch)
143            } else {
144                path.to_string()
145            };
146
147            if let Some(subpath) = parsed_vcs.subpath {
148                path.push_str(&format!("/{}", subpath));
149            }
150
151            let url = format!(
152                "https://{}/{}",
153                parsed_url.host_str().unwrap(),
154                path.trim_start_matches('/')
155            );
156
157            Some(Url::parse(&url).unwrap())
158        }
159        host if (host == "code.launchpad.net" || host == "launchpad.net")
160            && parsed_vcs.branch.is_none()
161            && parsed_vcs.subpath.is_none() =>
162        {
163            let url = format!(
164                "https://code.launchpad.net/{}",
165                parsed_url.path().trim_start_matches('/')
166            );
167
168            Some(Url::parse(&url).unwrap())
169        }
170        "git.savannah.gnu.org" | "git.sv.gnu.org" => {
171            let mut path_elements = parsed_url.path_segments().unwrap().collect::<Vec<_>>();
172            if parsed_url.scheme() == "https" && path_elements.first() == Some(&"git") {
173                path_elements.remove(0);
174            }
175            // Why cgit and not gitweb?
176            path_elements.insert(0, "cgit");
177            Some(
178                Url::parse(&format!(
179                    "https://{}/{}",
180                    parsed_url.host_str().unwrap(),
181                    path_elements.join("/")
182                ))
183                .unwrap(),
184            )
185        }
186        "git.code.sf.net" | "git.code.sourceforge.net" => {
187            let path_elements = parsed_url.path_segments().unwrap().collect::<Vec<_>>();
188            if path_elements.first() != Some(&"p") {
189                return None;
190            }
191            let project = path_elements[1];
192            let repository = path_elements[2];
193            let mut path_elements = vec!["p", project, repository];
194            let branch = if let Some(branch) = parsed_vcs.branch {
195                Some(branch)
196            } else if parsed_vcs.subpath.is_some() {
197                Some("HEAD".to_string())
198            } else {
199                None
200            };
201
202            if let Some(branch) = branch.as_deref() {
203                path_elements.extend(["ci", branch, "tree"]);
204            }
205
206            if let Some(subpath) = parsed_vcs.subpath.as_ref() {
207                path_elements.push(subpath);
208            }
209
210            let url = format!("https://sourceforge.net/{}", path_elements.join("/"));
211            Some(Url::parse(&url).unwrap())
212        }
213        _ => None,
214    }
215}
216
217/// Canonicalize a VCS browser URL.
218pub fn canonicalize_vcs_browser_url(url: &str) -> String {
219    let url = url.replace(
220        "https://svn.debian.org/wsvn/",
221        "https://anonscm.debian.org/viewvc/",
222    );
223    let url = url.replace(
224        "http://svn.debian.org/wsvn/",
225        "https://anonscm.debian.org/viewvc/",
226    );
227    let url = url.replace(
228        "https://git.debian.org/?p=",
229        "https://anonscm.debian.org/git/",
230    );
231    let url = url.replace(
232        "http://git.debian.org/?p=",
233        "https://anonscm.debian.org/git/",
234    );
235    let url = url.replace(
236        "https://bzr.debian.org/loggerhead/",
237        "https://anonscm.debian.org/loggerhead/",
238    );
239    let url = url.replace(
240        "http://bzr.debian.org/loggerhead/",
241        "https://anonscm.debian.org/loggerhead/",
242    );
243
244    lazy_regex::regex_replace!(
245        r"^https?://salsa.debian.org/([^/]+/[^/]+)\.git/?$",
246        &url,
247        |_, x| "https://salsa.debian.org/".to_string() + x
248    )
249    .into_owned()
250}
251
252/// VCS information for a package.
253#[derive(Debug, PartialEq, Eq, Clone)]
254pub enum PackageVcs {
255    /// Git repository.
256    Git {
257        /// URL of the repository.
258        url: Url,
259
260        /// Branch name.
261        branch: Option<String>,
262
263        /// Subpath within the repository.
264        subpath: Option<std::path::PathBuf>,
265    },
266    /// Subversion repository.
267    Svn(Url),
268
269    /// Bazaar repository.
270    Bzr(Url),
271
272    /// Mercurial repository.
273    Hg {
274        /// URL of the repository.
275        url: Url,
276
277        /// Branch name.
278        branch: Option<String>,
279
280        /// Subpath within the repository.
281        subpath: Option<std::path::PathBuf>,
282    },
283
284    /// Monotone repository.
285    Mtn(Url),
286
287    /// CVS repository.
288    Cvs(String),
289
290    /// Darcs repository.
291    Darcs(Url),
292
293    /// Arch repository.
294    Arch(Url),
295
296    /// Svk repository.
297    Svk(Url),
298}
299
300impl PackageVcs {
301    /// Get the type of the VCS repository as a string.
302    pub fn type_str(&self) -> &str {
303        match self {
304            PackageVcs::Git { .. } => "Git",
305            PackageVcs::Svn(_) => "Svn",
306            PackageVcs::Bzr(_) => "Bzr",
307            PackageVcs::Hg { .. } => "Hg",
308            PackageVcs::Mtn(_) => "Mtn",
309            PackageVcs::Cvs(_) => "Cvs",
310            PackageVcs::Darcs(_) => "Darcs",
311            PackageVcs::Arch(_) => "Arch",
312            PackageVcs::Svk(_) => "Svk",
313        }
314    }
315
316    /// Get the URL of the VCS repository.
317    pub fn url(&self) -> Option<&url::Url> {
318        match self {
319            PackageVcs::Git { url, .. } => Some(url),
320            PackageVcs::Svn(url) => Some(url),
321            PackageVcs::Bzr(url) => Some(url),
322            PackageVcs::Hg { url, .. } => Some(url),
323            PackageVcs::Mtn(url) => Some(url),
324            PackageVcs::Darcs(url) => Some(url),
325            PackageVcs::Arch(url) => Some(url),
326            PackageVcs::Svk(url) => Some(url),
327            PackageVcs::Cvs(_) => None,
328        }
329    }
330
331    /// Get the branch name of the VCS repository.
332    pub fn branch(&self) -> Option<&str> {
333        match self {
334            PackageVcs::Git { branch, .. } => branch.as_deref(),
335            PackageVcs::Hg { branch, .. } => branch.as_deref(),
336            _ => None,
337        }
338    }
339
340    /// Get the subpath of the VCS repository.
341    pub fn subpath(&self) -> Option<&std::path::Path> {
342        match self {
343            PackageVcs::Git { subpath, .. } => subpath.as_deref(),
344            PackageVcs::Hg { subpath, .. } => subpath.as_deref(),
345            _ => None,
346        }
347    }
348
349    /// Get the location of the VCS repository.
350    pub fn location(&self) -> String {
351        match self {
352            PackageVcs::Git {
353                url,
354                branch,
355                subpath,
356            } => {
357                let mut result = url.to_string();
358                if let Some(branch) = branch {
359                    result.push_str(&format!(" -b {}", branch));
360                }
361                if let Some(subpath) = subpath {
362                    result.push_str(&format!(" [{}]", subpath.display()));
363                }
364                result
365            }
366            PackageVcs::Svn(url) => url.to_string(),
367            PackageVcs::Bzr(url) => url.to_string(),
368            PackageVcs::Hg {
369                url,
370                branch,
371                subpath,
372            } => {
373                let mut result = url.to_string();
374                if let Some(branch) = branch {
375                    result.push_str(&format!(" -b {}", branch));
376                }
377                if let Some(subpath) = subpath {
378                    result.push_str(&format!(" [{}]", subpath.display()));
379                }
380                result
381            }
382            PackageVcs::Mtn(url) => url.to_string(),
383            PackageVcs::Cvs(s) => s.clone(),
384            PackageVcs::Darcs(url) => url.to_string(),
385            PackageVcs::Arch(url) => url.to_string(),
386            PackageVcs::Svk(url) => url.to_string(),
387        }
388    }
389}
390
391impl From<PackageVcs> for ParsedVcs {
392    fn from(vcs: PackageVcs) -> Self {
393        match vcs {
394            PackageVcs::Git {
395                url,
396                branch,
397                subpath,
398            } => ParsedVcs {
399                repo_url: url.to_string(),
400                branch,
401                subpath: subpath.map(|x| x.to_string_lossy().to_string()),
402            },
403            PackageVcs::Svn(url) => ParsedVcs {
404                repo_url: url.to_string(),
405                branch: None,
406                subpath: None,
407            },
408            PackageVcs::Bzr(url) => ParsedVcs {
409                repo_url: url.to_string(),
410                branch: None,
411                subpath: None,
412            },
413            PackageVcs::Hg {
414                url,
415                branch,
416                subpath,
417            } => ParsedVcs {
418                repo_url: url.to_string(),
419                branch,
420                subpath: subpath.map(|x| x.to_string_lossy().to_string()),
421            },
422            PackageVcs::Mtn(url) => ParsedVcs {
423                repo_url: url.to_string(),
424                branch: None,
425                subpath: None,
426            },
427            PackageVcs::Cvs(s) => ParsedVcs {
428                repo_url: s,
429                branch: None,
430                subpath: None,
431            },
432            PackageVcs::Darcs(url) => ParsedVcs {
433                repo_url: url.to_string(),
434                branch: None,
435                subpath: None,
436            },
437            PackageVcs::Arch(url) => ParsedVcs {
438                repo_url: url.to_string(),
439                branch: None,
440                subpath: None,
441            },
442            PackageVcs::Svk(url) => ParsedVcs {
443                repo_url: url.to_string(),
444                branch: None,
445                subpath: None,
446            },
447        }
448    }
449}
450
451/// Trait for types that can provide VCS information.
452pub trait VcsSource {
453    /// Get the Vcs-Git field.
454    fn vcs_git(&self) -> Option<String>;
455
456    /// Get the Vcs-Svn field.
457    fn vcs_svn(&self) -> Option<String>;
458
459    /// Get the Vcs-Bzr field.
460    fn vcs_bzr(&self) -> Option<String>;
461
462    /// Get the Vcs-Hg field.
463    fn vcs_hg(&self) -> Option<String>;
464
465    /// Get the Vcs-Mtn field.
466    fn vcs_mtn(&self) -> Option<String>;
467
468    /// Get the Vcs-Cvs field.
469    fn vcs_cvs(&self) -> Option<String>;
470
471    /// Get the Vcs-Darcs field.
472    fn vcs_darcs(&self) -> Option<String>;
473
474    /// Get the Vcs-Arch field.
475    fn vcs_arch(&self) -> Option<String>;
476
477    /// Get the Vcs-Svk field.
478    fn vcs_svk(&self) -> Option<String>;
479}
480
481impl VcsSource for debian_control::Source {
482    fn vcs_git(&self) -> Option<String> {
483        self.vcs_git()
484    }
485
486    fn vcs_svn(&self) -> Option<String> {
487        self.vcs_svn()
488    }
489
490    fn vcs_bzr(&self) -> Option<String> {
491        self.vcs_bzr()
492    }
493
494    fn vcs_hg(&self) -> Option<String> {
495        self.vcs_hg()
496    }
497
498    fn vcs_mtn(&self) -> Option<String> {
499        self.vcs_mtn()
500    }
501
502    fn vcs_cvs(&self) -> Option<String> {
503        self.vcs_cvs()
504    }
505
506    fn vcs_darcs(&self) -> Option<String> {
507        self.vcs_darcs()
508    }
509
510    fn vcs_arch(&self) -> Option<String> {
511        self.vcs_arch()
512    }
513
514    fn vcs_svk(&self) -> Option<String> {
515        self.vcs_svk()
516    }
517}
518
519impl VcsSource for debian_control::apt::Source {
520    fn vcs_git(&self) -> Option<String> {
521        self.vcs_git()
522    }
523
524    fn vcs_svn(&self) -> Option<String> {
525        self.vcs_svn()
526    }
527
528    fn vcs_bzr(&self) -> Option<String> {
529        self.vcs_bzr()
530    }
531
532    fn vcs_hg(&self) -> Option<String> {
533        self.vcs_hg()
534    }
535
536    fn vcs_mtn(&self) -> Option<String> {
537        self.vcs_mtn()
538    }
539
540    fn vcs_cvs(&self) -> Option<String> {
541        self.vcs_cvs()
542    }
543
544    fn vcs_darcs(&self) -> Option<String> {
545        self.vcs_darcs()
546    }
547
548    fn vcs_arch(&self) -> Option<String> {
549        self.vcs_arch()
550    }
551
552    fn vcs_svk(&self) -> Option<String> {
553        self.vcs_svk()
554    }
555}
556
557/// Determine the VCS field for a source package.
558pub fn vcs_field(source_package: &impl VcsSource) -> Option<(String, String)> {
559    if let Some(value) = source_package.vcs_git() {
560        return Some(("Git".to_string(), value));
561    }
562    if let Some(value) = source_package.vcs_svn() {
563        return Some(("Svn".to_string(), value));
564    }
565    if let Some(value) = source_package.vcs_bzr() {
566        return Some(("Bzr".to_string(), value));
567    }
568    if let Some(value) = source_package.vcs_hg() {
569        return Some(("Hg".to_string(), value));
570    }
571    if let Some(value) = source_package.vcs_mtn() {
572        return Some(("Mtn".to_string(), value));
573    }
574    if let Some(value) = source_package.vcs_cvs() {
575        return Some(("Cvs".to_string(), value));
576    }
577    if let Some(value) = source_package.vcs_darcs() {
578        return Some(("Darcs".to_string(), value));
579    }
580    if let Some(value) = source_package.vcs_arch() {
581        return Some(("Arch".to_string(), value));
582    }
583    if let Some(value) = source_package.vcs_svk() {
584        return Some(("Svk".to_string(), value));
585    }
586    None
587}
588
589/// Determine the VCS URL for a source package.
590pub fn source_package_vcs(source_package: &impl VcsSource) -> Option<PackageVcs> {
591    if let Some(value) = source_package.vcs_git() {
592        let parsed_vcs: ParsedVcs = value.parse().unwrap();
593        let url = parsed_vcs.repo_url.parse().unwrap();
594        return Some(PackageVcs::Git {
595            url,
596            branch: parsed_vcs.branch,
597            subpath: parsed_vcs.subpath.map(std::path::PathBuf::from),
598        });
599    }
600    if let Some(value) = source_package.vcs_svn() {
601        let url = value.parse().unwrap();
602        return Some(PackageVcs::Svn(url));
603    }
604    if let Some(value) = source_package.vcs_bzr() {
605        let url = value.parse().unwrap();
606        return Some(PackageVcs::Bzr(url));
607    }
608    if let Some(value) = source_package.vcs_hg() {
609        let parsed_vcs: ParsedVcs = value.parse().unwrap();
610        let url = parsed_vcs.repo_url.parse().unwrap();
611        return Some(PackageVcs::Hg {
612            url,
613            branch: parsed_vcs.branch,
614            subpath: parsed_vcs.subpath.map(std::path::PathBuf::from),
615        });
616    }
617    if let Some(value) = source_package.vcs_mtn() {
618        let url = value.parse().unwrap();
619        return Some(PackageVcs::Mtn(url));
620    }
621    if let Some(value) = source_package.vcs_cvs() {
622        return Some(PackageVcs::Cvs(value.clone()));
623    }
624    if let Some(value) = source_package.vcs_darcs() {
625        let url = value.parse().unwrap();
626        return Some(PackageVcs::Darcs(url));
627    }
628    if let Some(value) = source_package.vcs_arch() {
629        let url = value.parse().unwrap();
630        return Some(PackageVcs::Arch(url));
631    }
632    if let Some(value) = source_package.vcs_svk() {
633        let url = value.parse().unwrap();
634        return Some(PackageVcs::Svk(url));
635    }
636    None
637}
638
639#[cfg(test)]
640mod tests {
641    #[test]
642    fn test_source_package_vcs() {
643        use super::PackageVcs;
644        use debian_control::Control;
645
646        let control: Control = r#"Source: foo
647Vcs-Git: https://salsa.debian.org/foo/bar.git
648"#
649        .parse()
650        .unwrap();
651        assert_eq!(
652            super::source_package_vcs(&control.source().unwrap()),
653            Some(PackageVcs::Git {
654                url: "https://salsa.debian.org/foo/bar.git".parse().unwrap(),
655                branch: None,
656                subpath: None
657            })
658        );
659
660        let control: Control = r#"Source: foo
661Vcs-Svn: https://svn.debian.org/svn/foo/bar
662"#
663        .parse()
664        .unwrap();
665        assert_eq!(
666            super::source_package_vcs(&control.source().unwrap()),
667            Some(PackageVcs::Svn(
668                "https://svn.debian.org/svn/foo/bar".parse().unwrap()
669            ))
670        );
671    }
672
673    #[test]
674    fn test_determine_gitlab_browser_url() {
675        use super::determine_gitlab_browser_url;
676
677        assert_eq!(
678            determine_gitlab_browser_url("https://salsa.debian.org/foo/bar"),
679            "https://salsa.debian.org/foo/bar".parse().unwrap()
680        );
681
682        assert_eq!(
683            determine_gitlab_browser_url("https://salsa.debian.org/foo/bar.git"),
684            "https://salsa.debian.org/foo/bar".parse().unwrap()
685        );
686
687        assert_eq!(
688            determine_gitlab_browser_url("https://salsa.debian.org/foo/bar/"),
689            "https://salsa.debian.org/foo/bar".parse().unwrap()
690        );
691
692        assert_eq!(
693            determine_gitlab_browser_url("https://salsa.debian.org/foo/bar/.git"),
694            "https://salsa.debian.org/foo/bar/".parse().unwrap()
695        );
696
697        assert_eq!(
698            determine_gitlab_browser_url("https://salsa.debian.org/foo/bar.git -b baz"),
699            "https://salsa.debian.org/foo/bar/-/tree/baz"
700                .parse()
701                .unwrap()
702        );
703
704        assert_eq!(
705            determine_gitlab_browser_url(
706                "https://salsa.debian.org/foo/bar.git/ -b baz [otherpath]"
707            ),
708            "https://salsa.debian.org/foo/bar/-/tree/baz/otherpath"
709                .parse()
710                .unwrap()
711        );
712    }
713
714    #[test]
715    fn test_determine_browser_url() {
716        use super::determine_browser_url;
717        use url::Url;
718
719        assert_eq!(
720            determine_browser_url("git", "https://salsa.debian.org/foo/bar", Some(false)),
721            Some(Url::parse("https://salsa.debian.org/foo/bar").unwrap())
722        );
723        assert_eq!(
724            determine_browser_url("git", "https://salsa.debian.org/foo/bar.git", Some(false)),
725            Some(Url::parse("https://salsa.debian.org/foo/bar").unwrap())
726        );
727        assert_eq!(
728            determine_browser_url("git", "https://salsa.debian.org/foo/bar/", Some(false)),
729            Some(Url::parse("https://salsa.debian.org/foo/bar").unwrap())
730        );
731        assert_eq!(
732            determine_browser_url("git", "https://salsa.debian.org/foo/bar/.git", Some(false)),
733            Some(Url::parse("https://salsa.debian.org/foo/bar/").unwrap())
734        );
735        assert_eq!(
736            determine_browser_url("git", "https://salsa.debian.org/foo/bar.git/", Some(false)),
737            Some(Url::parse("https://salsa.debian.org/foo/bar").unwrap())
738        );
739        assert_eq!(
740            determine_browser_url(
741                "git",
742                "https://salsa.debian.org/foo/bar.git/.git",
743                Some(false)
744            ),
745            Some(Url::parse("https://salsa.debian.org/foo/bar.git/").unwrap())
746        );
747        assert_eq!(
748            determine_browser_url(
749                "git",
750                "https://salsa.debian.org/foo/bar.git.git",
751                Some(false)
752            ),
753            Some(Url::parse("https://salsa.debian.org/foo/bar").unwrap())
754        );
755        assert_eq!(
756            determine_browser_url(
757                "git",
758                "https://salsa.debian.org/foo/bar.git.git/",
759                Some(false)
760            ),
761            Some(Url::parse("https://salsa.debian.org/foo/bar").unwrap())
762        );
763
764        assert_eq!(
765            Some(Url::parse("https://salsa.debian.org/jelmer/dulwich").unwrap()),
766            determine_browser_url(
767                "git",
768                "https://salsa.debian.org/jelmer/dulwich.git",
769                Some(false)
770            )
771        );
772
773        assert_eq!(
774            Some(Url::parse("https://github.com/jelmer/dulwich").unwrap()),
775            determine_browser_url("git", "https://github.com/jelmer/dulwich.git", Some(false))
776        );
777        assert_eq!(
778            Some(Url::parse("https://github.com/jelmer/dulwich/tree/master").unwrap()),
779            determine_browser_url(
780                "git",
781                "https://github.com/jelmer/dulwich.git -b master",
782                Some(false)
783            )
784        );
785        assert_eq!(
786            Some(Url::parse("https://github.com/jelmer/dulwich/tree/master").unwrap()),
787            determine_browser_url(
788                "git",
789                "git://github.com/jelmer/dulwich -b master",
790                Some(false)
791            ),
792        );
793        assert_eq!(
794            Some(Url::parse("https://github.com/jelmer/dulwich/tree/master/blah").unwrap()),
795            determine_browser_url(
796                "git",
797                "git://github.com/jelmer/dulwich -b master [blah]",
798                Some(false)
799            ),
800        );
801        assert_eq!(
802            Some(Url::parse("https://github.com/jelmer/dulwich/tree/HEAD/blah").unwrap()),
803            determine_browser_url("git", "git://github.com/jelmer/dulwich [blah]", Some(false)),
804        );
805        assert_eq!(
806            Some(Url::parse("https://git.sv.gnu.org/cgit/rcs.git").unwrap()),
807            determine_browser_url("git", "https://git.sv.gnu.org/git/rcs.git", Some(false)),
808        );
809        assert_eq!(
810            Some(Url::parse("https://git.savannah.gnu.org/cgit/rcs.git").unwrap()),
811            determine_browser_url("git", "git://git.savannah.gnu.org/rcs.git", Some(false)),
812        );
813        assert_eq!(
814            Some(Url::parse("https://sourceforge.net/p/shorewall/debian").unwrap()),
815            determine_browser_url(
816                "git",
817                "git://git.code.sf.net/p/shorewall/debian",
818                Some(false)
819            ),
820        );
821        assert_eq!(
822            Some(Url::parse("https://sourceforge.net/p/shorewall/debian/ci/foo/tree").unwrap()),
823            determine_browser_url(
824                "git",
825                "git://git.code.sf.net/p/shorewall/debian -b foo",
826                Some(false)
827            ),
828        );
829        assert_eq!(
830            Some(Url::parse("https://sourceforge.net/p/shorewall/debian/ci/HEAD/tree/sp").unwrap()),
831            determine_browser_url(
832                "git",
833                "git://git.code.sf.net/p/shorewall/debian [sp]",
834                Some(false)
835            ),
836        );
837        assert_eq!(
838            Some(Url::parse("https://sourceforge.net/p/shorewall/debian/ci/foo/tree/sp").unwrap()),
839            determine_browser_url(
840                "git",
841                "git://git.code.sf.net/p/shorewall/debian -b foo [sp]",
842                Some(false)
843            ),
844        );
845    }
846
847    #[test]
848    fn test_vcs_field() {
849        use debian_control::Control;
850
851        let control: Control = r#"Source: foo
852Vcs-Git: https://salsa.debian.org/foo/bar.git
853"#
854        .parse()
855        .unwrap();
856        assert_eq!(
857            super::vcs_field(&control.source().unwrap()),
858            Some((
859                "Git".to_string(),
860                "https://salsa.debian.org/foo/bar.git".to_string()
861            ))
862        );
863    }
864}