1use std::collections::HashMap;
2use std::process::Command;
3
4use reqwest::blocking::Client;
5use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
6use semver::Version;
7use serde::Deserialize;
8
9#[derive(Deserialize, Debug)]
10pub struct IntermediaryTags(Vec<IntermediaryTag>);
11
12#[derive(Debug)]
13pub struct Tags {
14 versions: Vec<Version>,
15 prefix: String,
16}
17
18impl Tags {
19 pub fn get_latest_tag(&mut self) -> Option<String> {
20 self.sort();
21 let mut buf = String::new();
22 buf.push_str(&self.prefix);
23 let latest_version = &self.versions.iter().last()?;
24 buf.push_str(&latest_version.to_string());
25 Some(buf)
26 }
27 pub fn sort(&mut self) {
28 self.versions.sort_by(Version::cmp_precedence);
29 }
30}
31
32#[derive(Deserialize, Debug)]
33pub struct IntermediaryTag {
34 name: String,
35}
36
37pub fn get_tags(repo: &str, owner: &str) -> Result<Tags, ()> {
39 let tags = query_tags(repo, owner).unwrap();
40 Ok(tags.into())
41}
42
43#[derive(Deserialize, Debug, Clone)]
44struct NixConfig {
45 #[serde(rename = "access-tokens")]
46 access_tokens: Option<AccessTokens>,
47}
48
49impl NixConfig {
50 fn gh_token(&self) -> Option<String> {
51 self.access_tokens
52 .clone()
53 .unwrap()
54 .value
55 .get("github.com")
56 .cloned()
57 }
58}
59
60#[derive(Deserialize, Debug, Clone)]
61struct AccessTokens {
62 value: HashMap<String, String>,
63}
64
65pub fn get_gh_token() -> Option<String> {
67 let command = Command::new("nix")
68 .arg("config")
69 .arg("show")
70 .arg("--json")
71 .output()
72 .unwrap();
73 let stdout = String::from_utf8(command.stdout).unwrap();
74 let output: NixConfig = serde_json::from_str(&stdout).unwrap();
75
76 if let Some(token) = output.gh_token() {
77 return Some(token);
78 };
79 if let Ok(token) = std::env::var("GITHUB_TOKEN") {
80 return Some(token);
81 };
82
83 None
84}
85
86fn query_tags(repo: &str, owner: &str) -> Result<IntermediaryTags, ()> {
90 let client = Client::new();
91 let mut headers = HeaderMap::new();
92 headers.insert(USER_AGENT, HeaderValue::from_str("flake-edit").unwrap());
93 if let Some(token) = get_gh_token() {
94 tracing::debug!("Found github token.");
95 headers.insert(
96 AUTHORIZATION,
97 HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
98 );
99 tracing::debug!("Settings github token.");
100 }
101 let body = client
102 .get(format!(
103 "https://api.github.com/repos/{}/{}/tags",
104 repo, owner
105 ))
106 .headers(headers)
107 .send()
108 .unwrap()
109 .text()
110 .unwrap();
111
112 tracing::debug!("Body from api: {body}");
113
114 match serde_json::from_str::<IntermediaryTags>(&body) {
115 Ok(tags) => Ok(tags),
116 Err(e) => {
117 tracing::error!("Error from api: {e}");
118 Err(())
119 }
120 }
121}
122
123impl From<IntermediaryTags> for Tags {
124 fn from(value: IntermediaryTags) -> Self {
125 fn strip_until_char(s: &str, c: char) -> Option<(String, String)> {
126 s.find(c).map(|index| {
127 let prefix = s[..index].to_string();
128 let remaining = s[index + 1..].to_string();
129 (prefix, remaining)
130 })
131 }
132 let mut versions = vec![];
133 let mut prefix = String::new();
134 for itag in value.0 {
135 let mut tag = itag.name;
136 if let Some(new_tag) = tag.strip_prefix('v') {
138 tag = new_tag.to_string();
139 prefix = "v".to_string();
140 }
141
142 if let Some((new_prefix, new_tag)) = strip_until_char(&tag, '-') {
143 tag = new_tag;
144 prefix = format!("{new_prefix}-").to_string();
145 }
146
147 match Version::parse(&tag) {
148 Ok(semver) => {
149 versions.push(semver);
150 }
151 Err(e) => {
152 tracing::error!("Could not parse version {:?}", e);
153 }
154 }
155 }
156 Tags { versions, prefix }
157 }
158}