cargo_plugin_utils/
common.rs1use std::env;
4
5use anyhow::{
6 Context,
7 Result,
8};
9use cargo_metadata::MetadataCommand;
10
11#[allow(clippy::disallowed_methods)] pub fn detect_repo() -> Result<(String, String)> {
14 if let Ok(repo) = env::var("GITHUB_REPOSITORY") {
16 let parts: Vec<&str> = repo.split('/').collect();
17 if parts.len() == 2 {
18 return Ok((parts[0].to_string(), parts[1].to_string()));
19 }
20 }
21
22 let repo = gix::discover(".").context("Failed to discover git repository")?;
24 let remote = repo
25 .find_default_remote(gix::remote::Direction::Fetch)
26 .context("Failed to find default remote")?
27 .context("No default remote found")?;
28
29 let remote_url = remote
30 .url(gix::remote::Direction::Fetch)
31 .context("Failed to get remote URL")?;
32
33 let url_str = remote_url.to_string();
35 if let Some(rest) = url_str.strip_prefix("git@github.com:") {
36 let rest_trimmed: &str = rest.strip_suffix(".git").unwrap_or(rest);
37 let parts: Vec<&str> = rest_trimmed.split('/').collect();
38 if parts.len() >= 2 {
39 return Ok((parts[0].to_string(), parts[1].to_string()));
40 }
41 } else if let Some(rest) = url_str.strip_prefix("https://github.com/") {
42 let rest_trimmed: &str = rest.strip_suffix(".git").unwrap_or(rest);
43 let parts: Vec<&str> = rest_trimmed.split('/').collect();
44 if parts.len() >= 2 {
45 return Ok((parts[0].to_string(), parts[1].to_string()));
46 }
47 }
48
49 anyhow::bail!(
50 "Could not detect GitHub repository. Set GITHUB_REPOSITORY or use --owner/--repo flags"
51 );
52}
53
54pub fn get_owner_repo(owner: Option<String>, repo: Option<String>) -> Result<(String, String)> {
56 match (owner, repo) {
57 (Some(o), Some(r)) => Ok((o, r)),
58 (Some(_), None) | (None, Some(_)) => {
59 anyhow::bail!("Both --owner and --repo must be provided together");
60 }
61 (None, None) => detect_repo(),
62 }
63}
64
65pub fn find_package(manifest_path: Option<&std::path::Path>) -> Result<cargo_metadata::Package> {
77 let mut cmd = MetadataCommand::new();
78 if let Some(path) = manifest_path {
79 cmd.manifest_path(path);
80 }
81
82 let metadata = cmd.exec().context("Failed to get cargo metadata")?;
83
84 let current_dir = std::env::current_dir().context("Failed to get current directory")?;
86
87 let canonical_current_dir = current_dir.canonicalize().ok();
89 let packages_with_dirs: Vec<_> = metadata
90 .packages
91 .iter()
92 .filter_map(|pkg| {
93 pkg.manifest_path
95 .as_std_path()
96 .parent()
97 .and_then(|p| p.canonicalize().ok())
98 .map(|p| (pkg.clone(), p))
99 })
100 .collect();
101
102 if let Some(ref canonical_current) = canonical_current_dir
104 && let Some((pkg, _)) = packages_with_dirs
105 .iter()
106 .find(|(_, pkg_dir)| pkg_dir == canonical_current)
107 {
108 return Ok(pkg.clone());
109 }
110
111 let current_manifest = current_dir.join("Cargo.toml");
114 let canonical_current_manifest = current_manifest.canonicalize().ok();
115 let packages_with_manifests: Vec<_> = metadata
116 .packages
117 .iter()
118 .filter_map(|pkg| {
119 pkg.manifest_path
120 .as_std_path()
121 .canonicalize()
122 .ok()
123 .map(|p| (pkg.clone(), p))
124 })
125 .collect();
126
127 if let Some(ref canonical) = canonical_current_manifest
128 && let Some((pkg, _)) = packages_with_manifests
129 .iter()
130 .find(|(_, pkg_path)| pkg_path == canonical)
131 {
132 return Ok(pkg.clone());
133 }
134
135 if let Some(root_package) = metadata.root_package() {
137 return Ok(root_package.clone());
138 }
139
140 if metadata.workspace_default_members.is_available()
146 && !metadata.workspace_default_members.is_empty()
147 && let Some(first_default_id) = metadata.workspace_default_members.first()
148 && let Some(default_package) = metadata
149 .packages
150 .iter()
151 .find(|pkg| &pkg.id == first_default_id)
152 {
153 return Ok(default_package.clone());
154 }
155
156 anyhow::bail!(
158 "No package found in current directory. Run this command from a package directory, \
159 or use --manifest-path to specify a package."
160 )
161}
162
163pub fn get_package_version_from_manifest(manifest_path: &std::path::Path) -> Result<String> {
165 let package = find_package(Some(manifest_path))?;
166 Ok(package.version.to_string())
167}
168
169pub fn get_metadata(manifest_path: Option<&std::path::Path>) -> Result<cargo_metadata::Metadata> {
175 let mut cmd = MetadataCommand::new();
176 if let Some(path) = manifest_path {
177 cmd.manifest_path(path);
178 }
179 cmd.exec().context("Failed to get cargo metadata")
180}
181
182pub fn get_workspace_packages(
187 manifest_path: Option<&std::path::Path>,
188) -> Result<Vec<cargo_metadata::Package>> {
189 let metadata = get_metadata(manifest_path)?;
190 Ok(metadata.packages)
191}
192
193#[cfg(test)]
194mod tests {
195 use std::env;
196
197 use super::*;
198
199 #[test]
200 fn test_get_owner_repo_both_provided() {
201 let result = get_owner_repo(Some("owner".to_string()), Some("repo".to_string()));
202 assert!(result.is_ok());
203 assert_eq!(result.unwrap(), ("owner".to_string(), "repo".to_string()));
204 }
205
206 #[test]
207 fn test_get_owner_repo_only_owner() {
208 let result = get_owner_repo(Some("owner".to_string()), None);
209 assert!(result.is_err());
210 assert!(
211 result
212 .unwrap_err()
213 .to_string()
214 .contains("Both --owner and --repo must be provided")
215 );
216 }
217
218 #[test]
219 fn test_get_owner_repo_only_repo() {
220 let result = get_owner_repo(None, Some("repo".to_string()));
221 assert!(result.is_err());
222 assert!(
223 result
224 .unwrap_err()
225 .to_string()
226 .contains("Both --owner and --repo must be provided")
227 );
228 }
229
230 #[test]
231 fn test_get_owner_repo_from_env() {
232 let original = env::var("GITHUB_REPOSITORY").ok();
234
235 unsafe {
237 env::set_var("GITHUB_REPOSITORY", "test-owner/test-repo");
238 }
239 let result = get_owner_repo(None, None);
240 assert!(result.is_ok());
241 assert_eq!(
242 result.unwrap(),
243 ("test-owner".to_string(), "test-repo".to_string())
244 );
245
246 unsafe {
248 if let Some(val) = original {
249 env::set_var("GITHUB_REPOSITORY", &val);
250 } else {
251 env::remove_var("GITHUB_REPOSITORY");
252 }
253 }
254 }
255
256 #[test]
257 fn test_get_owner_repo_invalid_env() {
258 unsafe {
260 env::set_var("GITHUB_REPOSITORY", "invalid");
261 }
262 let _result = get_owner_repo(None, None);
263 unsafe {
265 env::remove_var("GITHUB_REPOSITORY");
266 }
267 }
268
269 #[test]
270 fn test_find_package_in_current_dir() {
271 let result = find_package(None);
274 if let Err(e) = result {
276 assert!(e.to_string().contains("package") || e.to_string().contains("manifest"));
277 }
278 }
279
280 #[test]
281 fn test_find_package_with_manifest_path() {
282 let result = find_package(Some(std::path::Path::new("/nonexistent/path/Cargo.toml")));
284 assert!(result.is_err());
285 }
286
287 #[test]
288 fn test_get_package_version_from_manifest() {
289 let result =
291 get_package_version_from_manifest(std::path::Path::new("/nonexistent/path/Cargo.toml"));
292 assert!(result.is_err());
293 }
294
295 #[test]
296 fn test_detect_repo_from_env() {
297 let original = env::var("GITHUB_REPOSITORY").ok();
299
300 unsafe {
301 env::set_var("GITHUB_REPOSITORY", "env-owner/env-repo");
302 }
303 let result = detect_repo();
304 assert!(result.is_ok());
306 let (owner, repo) = result.unwrap();
307 assert_eq!(owner, "env-owner");
308 assert_eq!(repo, "env-repo");
309
310 unsafe {
312 if let Some(val) = original {
313 env::set_var("GITHUB_REPOSITORY", &val);
314 } else {
315 env::remove_var("GITHUB_REPOSITORY");
316 }
317 }
318 }
319
320 #[test]
321 fn test_detect_repo_invalid_env_format() {
322 unsafe {
323 env::set_var("GITHUB_REPOSITORY", "invalid-format");
324 }
325 let _result = detect_repo();
326 unsafe {
328 env::remove_var("GITHUB_REPOSITORY");
329 }
330 }
331}