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