Skip to main content

diffler_ci/
detect.rs

1//! Pick the CI provider for a repo: the git remote host first, then a config
2//! file fallback. A self-hosted GitLab host is carried through so adapters can
3//! target it. Network probing for ambiguous self-hosted hosts is a later
4//! refinement; an explicit config override always wins upstream.
5
6use std::path::Path;
7
8use crate::provider::ProviderKind;
9
10/// Detected provider plus an optional self-hosted host override (set only when
11/// the remote isn't a known SaaS host).
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Detected {
14    pub kind: ProviderKind,
15    pub host: Option<String>,
16}
17
18/// Detect from the repo on disk. `remote_host` is the host of the repo's `origin`
19/// remote (e.g. `github.com`); `forced` is a config override of the kind.
20pub fn detect(
21    repo_root: &Path,
22    remote_host: Option<&str>,
23    forced: Option<ProviderKind>,
24) -> Option<Detected> {
25    let has_gh = repo_root.join(".github/workflows").is_dir();
26    let has_gitlab = repo_root.join(".gitlab-ci.yml").is_file();
27    classify(remote_host, has_gh, has_gitlab, forced)
28}
29
30/// Pure classification from the gathered signals, so it can be tested without a
31/// filesystem.
32fn classify(
33    remote_host: Option<&str>,
34    has_gh_workflows: bool,
35    has_gitlab_ci: bool,
36    forced: Option<ProviderKind>,
37) -> Option<Detected> {
38    let self_hosted = |host: Option<&str>| {
39        host.filter(|h| !h.eq_ignore_ascii_case("gitlab.com"))
40            .map(str::to_owned)
41    };
42
43    // a forced kind takes the host from config (`[ci.<p>] host`), not the remote
44    if let Some(kind) = forced {
45        return Some(Detected { kind, host: None });
46    }
47
48    if let Some(host) = remote_host {
49        if host.eq_ignore_ascii_case("github.com") {
50            return Some(Detected {
51                kind: ProviderKind::GitHub,
52                host: None,
53            });
54        }
55        if host.eq_ignore_ascii_case("gitlab.com") || host.to_ascii_lowercase().contains("gitlab") {
56            return Some(Detected {
57                kind: ProviderKind::GitLab,
58                host: self_hosted(Some(host)),
59            });
60        }
61    }
62
63    // unknown host: fall back to config-file presence
64    if has_gh_workflows {
65        return Some(Detected {
66            kind: ProviderKind::GitHub,
67            host: None,
68        });
69    }
70    if has_gitlab_ci {
71        return Some(Detected {
72            kind: ProviderKind::GitLab,
73            host: self_hosted(remote_host),
74        });
75    }
76    None
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn github_dot_com_is_github() {
85        assert_eq!(
86            classify(Some("github.com"), false, false, None),
87            Some(Detected {
88                kind: ProviderKind::GitHub,
89                host: None
90            })
91        );
92    }
93
94    #[test]
95    fn gitlab_dot_com_carries_no_host() {
96        assert_eq!(
97            classify(Some("gitlab.com"), false, false, None),
98            Some(Detected {
99                kind: ProviderKind::GitLab,
100                host: None
101            })
102        );
103    }
104
105    #[test]
106    fn self_hosted_gitlab_host_with_config_file() {
107        assert_eq!(
108            classify(Some("git.example.com"), false, true, None),
109            Some(Detected {
110                kind: ProviderKind::GitLab,
111                host: Some("git.example.com".into())
112            })
113        );
114    }
115
116    #[test]
117    fn unknown_host_falls_back_to_workflows_dir() {
118        assert_eq!(
119            classify(Some("git.example.com"), true, false, None),
120            Some(Detected {
121                kind: ProviderKind::GitHub,
122                host: None
123            })
124        );
125    }
126
127    #[test]
128    fn no_signals_is_none() {
129        assert_eq!(classify(None, false, false, None), None);
130    }
131
132    #[test]
133    fn forced_overrides_detection() {
134        assert_eq!(
135            classify(Some("github.com"), true, false, Some(ProviderKind::GitLab)),
136            Some(Detected {
137                kind: ProviderKind::GitLab,
138                host: None
139            })
140        );
141    }
142}