cargo_version_info/
github.rs1use std::env;
4
5use anyhow::{
6 Context,
7 Result,
8};
9
10use crate::version::{
11 format_version,
12 increment_patch,
13 parse_version,
14};
15
16#[allow(clippy::disallowed_methods)] pub async fn get_latest_release_version(
23 owner: &str,
24 repo: &str,
25 github_token: Option<&str>,
26) -> Result<Option<String>> {
27 let env_token = env::var("GITHUB_TOKEN").ok();
29 let token = github_token.or(env_token.as_deref());
30
31 let result = if let Some(token) = token {
34 get_latest_release_via_api(owner, repo, Some(token)).await
35 } else {
36 get_latest_release_via_api(owner, repo, None).await
38 };
39
40 match result {
41 Ok(version) => Ok(Some(version)),
42 Err(e) => {
43 let error_msg = e.to_string();
44 if error_msg.contains("No releases found") {
46 Ok(None)
47 } else if error_msg.contains("404") || error_msg.contains("Not Found") {
48 if token.is_none() {
50 Err(anyhow::anyhow!(
51 "Repository not found or is private. For private repositories, \
52 set GITHUB_TOKEN environment variable or pass --github-token"
53 )
54 .context(error_msg))
55 } else {
56 Err(e)
57 }
58 } else if error_msg.contains("403") || error_msg.contains("Forbidden") {
59 Err(anyhow::anyhow!(
61 "Access forbidden. This may be a private repository. \
62 Ensure GITHUB_TOKEN has appropriate permissions."
63 )
64 .context(error_msg))
65 } else {
66 Err(e)
67 }
68 }
69 }
70}
71
72async fn get_latest_release_via_api(
77 owner: &str,
78 repo: &str,
79 token: Option<&str>,
80) -> Result<String> {
81 let octocrab = if let Some(token) = token {
82 octocrab::OctocrabBuilder::new()
83 .personal_token(token.to_string())
84 .build()
85 .context("Failed to create GitHub API client")?
86 } else {
87 octocrab::Octocrab::builder()
89 .build()
90 .context("Failed to create GitHub API client")?
91 };
92
93 let releases = octocrab
94 .repos(owner, repo)
95 .releases()
96 .list()
97 .per_page(1)
98 .send()
99 .await
100 .context("Failed to query GitHub releases")?;
101
102 let release = releases.items.first().context("No releases found")?;
103
104 let tag_name = release.tag_name.as_str();
105 let version = tag_name.strip_prefix('v').unwrap_or(tag_name);
106 let version = version.strip_prefix('V').unwrap_or(version);
107
108 Ok(version.to_string())
109}
110
111fn get_latest_git_tag_version() -> Result<Option<String>> {
116 let cwd = std::env::current_dir().context("Failed to get current directory")?;
117 let repo = gix::discover(cwd)
118 .context("Failed to discover git repository. Ensure you're in a git repository.")?;
119
120 let mut version_tags: Vec<(String, (u32, u32, u32))> = repo
121 .references()?
122 .prefixed("refs/tags/")?
123 .filter_map(|r: Result<gix::Reference<'_>, _>| r.ok())
124 .filter_map(|r| {
125 let name_full = r.name().as_bstr().to_string();
126 let name = name_full.strip_prefix("refs/tags/").unwrap_or(&name_full);
127 let version_str = name
128 .strip_prefix('v')
129 .or_else(|| name.strip_prefix('V'))
130 .unwrap_or(name);
131
132 if let Ok((major, minor, patch)) = parse_version(version_str) {
134 Some((name.to_string(), (major, minor, patch)))
135 } else {
136 None
137 }
138 })
139 .collect();
140
141 version_tags.sort_by_key(|a| a.1);
143
144 Ok(version_tags
145 .last()
146 .map(|(tag_name, _): &(String, (u32, u32, u32))| {
147 tag_name
148 .strip_prefix('v')
149 .or_else(|| tag_name.strip_prefix('V'))
150 .unwrap_or(tag_name)
151 .to_string()
152 }))
153}
154
155pub async fn calculate_next_version(
161 _owner: &str,
162 _repo: &str,
163 _github_token: Option<&str>,
164) -> Result<(String, String)> {
165 let latest_version_str = match get_latest_git_tag_version()? {
167 Some(v) => v,
168 None => {
169 return Ok(("0.0.0".to_string(), "0.0.1".to_string()));
171 }
172 };
173
174 let (major, minor, patch) = parse_version(&latest_version_str)
175 .with_context(|| format!("Failed to parse latest version: {}", latest_version_str))?;
176
177 let (major, minor, patch) = increment_patch(major, minor, patch);
178 let next_version = format_version(major, minor, patch);
179
180 Ok((latest_version_str, next_version))
181}
182
183#[cfg(test)]
184mod tests {
185 use std::process::Command;
186
187 use tempfile::TempDir;
188
189 use super::*;
190
191 fn create_test_git_repo_with_tags(tags: &[&str]) -> TempDir {
192 let dir = tempfile::tempdir().unwrap();
193
194 Command::new("git")
196 .arg("init")
197 .current_dir(dir.path())
198 .output()
199 .unwrap();
200
201 Command::new("git")
202 .args(["config", "user.email", "test@example.com"])
203 .current_dir(dir.path())
204 .output()
205 .unwrap();
206
207 Command::new("git")
208 .args(["config", "user.name", "Test User"])
209 .current_dir(dir.path())
210 .output()
211 .unwrap();
212
213 std::fs::write(dir.path().join("README.md"), "# Test\n").unwrap();
215 Command::new("git")
216 .args(["add", "README.md"])
217 .current_dir(dir.path())
218 .output()
219 .unwrap();
220
221 Command::new("git")
222 .args(["commit", "-m", "Initial commit"])
223 .current_dir(dir.path())
224 .output()
225 .unwrap();
226
227 for tag in tags {
229 Command::new("git")
230 .args(["tag", "-a", tag, "-m", &format!("Release {}", tag)])
231 .current_dir(dir.path())
232 .output()
233 .unwrap();
234 }
235
236 dir
237 }
238
239 #[test]
240 fn test_get_latest_git_tag_version_no_tags() {
241 let dir = create_test_git_repo_with_tags(&[]);
242 let original_dir = std::env::current_dir().unwrap();
243
244 std::env::set_current_dir(dir.path()).unwrap();
245 let result = get_latest_git_tag_version().unwrap();
246 std::env::set_current_dir(original_dir).unwrap();
247
248 assert_eq!(result, None);
249 }
250
251 #[test]
252 fn test_get_latest_git_tag_version_single_tag() {
253 let _dir = create_test_git_repo_with_tags(&["v0.1.0"]);
254 let dir_path = _dir.path().to_path_buf();
255 let original_dir = std::env::current_dir().unwrap();
256
257 std::env::set_current_dir(&dir_path).unwrap();
258 let result = get_latest_git_tag_version().unwrap();
259 std::env::set_current_dir(original_dir).unwrap();
260
261 assert_eq!(result, Some("0.1.0".to_string()));
262 }
263
264 #[test]
265 fn test_get_latest_git_tag_version_multiple_tags() {
266 let _dir = create_test_git_repo_with_tags(&["v0.1.0", "v0.2.0", "v0.1.5"]);
267 let dir_path = _dir.path().to_path_buf();
268 let original_dir = std::env::current_dir().unwrap();
269
270 std::env::set_current_dir(&dir_path).unwrap();
271 let result = get_latest_git_tag_version().unwrap();
272 std::env::set_current_dir(original_dir).unwrap();
273
274 assert_eq!(result, Some("0.2.0".to_string()));
276 }
277
278 #[test]
279 fn test_get_latest_git_tag_version_without_v_prefix() {
280 let _dir = create_test_git_repo_with_tags(&["0.3.0", "v0.2.0"]);
281 let dir_path = _dir.path().to_path_buf();
282 let original_dir = std::env::current_dir().unwrap();
283
284 std::env::set_current_dir(&dir_path).unwrap();
285 let result = get_latest_git_tag_version().unwrap();
286 std::env::set_current_dir(original_dir).unwrap();
287
288 assert_eq!(result, Some("0.3.0".to_string()));
290 }
291
292 #[tokio::test]
293 async fn test_calculate_next_version_no_tags() {
294 let _dir = create_test_git_repo_with_tags(&[]);
295 let dir_path = _dir.path().to_path_buf();
296 let original_dir = std::env::current_dir().unwrap();
297
298 std::env::set_current_dir(&dir_path).unwrap();
299 let (latest, next) = calculate_next_version("test", "repo", None).await.unwrap();
300 std::env::set_current_dir(original_dir).unwrap();
301
302 assert_eq!(latest, "0.0.0");
303 assert_eq!(next, "0.0.1");
304 }
305
306 #[tokio::test]
307 async fn test_calculate_next_version_with_tags() {
308 let _dir = create_test_git_repo_with_tags(&["v0.1.2"]);
309 let dir_path = _dir.path().to_path_buf();
310 let original_dir = std::env::current_dir().unwrap();
311
312 std::env::set_current_dir(&dir_path).unwrap();
313 let (latest, next) = calculate_next_version("test", "repo", None).await.unwrap();
314 std::env::set_current_dir(original_dir).unwrap();
315
316 assert_eq!(latest, "0.1.2");
317 assert_eq!(next, "0.1.3");
318 }
319
320 #[tokio::test]
321 #[ignore] async fn test_get_latest_release_via_api() {
323 if let Ok(Some(version)) = get_latest_release_version("rust-lang", "rust", None).await {
326 println!("Latest rust release: {}", version);
327 }
328 }
329}