1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase", tag = "type")]
15#[non_exhaustive]
16pub enum Flavor {
17 Forgejo {
19 major: u32,
21 minor: u32,
23 },
24 Gitea {
26 major: u32,
28 minor: u32,
30 },
31 Unknown,
34}
35
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39#[non_exhaustive]
40pub struct Features {
41 pub fast_forward_only: bool,
43 pub actions_variables: bool,
46 pub forgejo_webhooks: bool,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53#[non_exhaustive]
54pub struct InstanceInfo {
55 pub version: String,
57 pub flavor: Flavor,
59 pub features: Features,
61}
62
63impl InstanceInfo {
64 pub fn from_version(version: &str) -> Self {
66 let flavor = parse(version);
67 let at_least = |maj: u32, min: u32, (a, b): (u32, u32)| (a, b) >= (maj, min);
68 let features = match flavor {
69 Flavor::Forgejo { major, minor } => Features {
70 fast_forward_only: at_least(7, 0, (major, minor)),
71 actions_variables: at_least(8, 0, (major, minor)),
72 forgejo_webhooks: true,
73 },
74 Flavor::Gitea { major, minor } => Features {
75 fast_forward_only: at_least(1, 22, (major, minor)),
76 actions_variables: at_least(1, 22, (major, minor)),
77 forgejo_webhooks: false,
78 },
79 Flavor::Unknown => Features::default(),
80 };
81 InstanceInfo {
82 version: version.to_string(),
83 flavor,
84 features,
85 }
86 }
87}
88
89fn parse(version: &str) -> Flavor {
90 let (own, gitea) = match version.split_once("+gitea-") {
91 Some((own, gitea)) => (own, Some(gitea)),
92 None => (version, None),
93 };
94 let Some((major, minor)) = major_minor(own) else {
95 return Flavor::Unknown;
96 };
97 match gitea {
98 Some(_) => Flavor::Forgejo { major, minor },
99 None if major >= 7 => Flavor::Forgejo { major, minor },
102 None if major == 1 => Flavor::Gitea { major, minor },
103 None => Flavor::Unknown,
104 }
105}
106
107fn major_minor(v: &str) -> Option<(u32, u32)> {
108 let v = v.trim().trim_start_matches('v');
109 let mut parts = v.split(['.', '-', '+']);
110 let major = parts.next()?.parse().ok()?;
111 let minor = parts.next()?.parse().ok()?;
112 Some((major, minor))
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn versions_parse_and_gate_features() {
121 let f = InstanceInfo::from_version("9.0.0+gitea-1.22.0");
122 assert_eq!(f.flavor, Flavor::Forgejo { major: 9, minor: 0 });
123 assert!(f.features.fast_forward_only && f.features.actions_variables);
124 assert!(f.features.forgejo_webhooks);
125
126 let dev = InstanceInfo::from_version("16.0.0-dev-753-6bcc6da0+gitea-1.22.0");
127 assert_eq!(
128 dev.flavor,
129 Flavor::Forgejo {
130 major: 16,
131 minor: 0
132 }
133 );
134
135 let seven = InstanceInfo::from_version("7.0.5+gitea-1.21.0");
136 assert!(seven.features.fast_forward_only && !seven.features.actions_variables);
137
138 let gitea = InstanceInfo::from_version("1.22.3");
139 assert_eq!(
140 gitea.flavor,
141 Flavor::Gitea {
142 major: 1,
143 minor: 22
144 }
145 );
146 assert!(gitea.features.fast_forward_only && !gitea.features.forgejo_webhooks);
147
148 let old = InstanceInfo::from_version("1.21.11-1");
149 assert_eq!(old.features, Features::default());
150
151 for junk in ["", "dev", "x.y.z", "3.0.0"] {
152 assert_eq!(InstanceInfo::from_version(junk).flavor, Flavor::Unknown);
153 }
154 }
155}