anodizer_core/git/
slug.rs1use anyhow::{Result, bail};
23use std::path::Path;
24
25use super::remote::{detect_github_repo_in, detect_owner_repo_in};
26
27#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct RepoSlug {
36 owner: String,
37 name: String,
38}
39
40impl RepoSlug {
41 pub fn owner(&self) -> &str {
44 &self.owner
45 }
46
47 pub fn name(&self) -> &str {
49 &self.name
50 }
51
52 pub fn slug(&self) -> String {
54 format!("{}/{}", self.owner, self.name)
55 }
56
57 fn validated(owner: String, name: String) -> Result<Self> {
62 if owner.trim().is_empty() || name.trim().is_empty() {
63 bail!(
64 "repository slug requires a non-empty owner and name (got {:?}/{:?})",
65 owner,
66 name
67 );
68 }
69 Ok(Self { owner, name })
70 }
71
72 #[cfg(any(test, feature = "test-helpers"))]
76 pub fn for_test(owner: impl Into<String>, name: impl Into<String>) -> Self {
77 Self {
78 owner: owner.into(),
79 name: name.into(),
80 }
81 }
82}
83
84fn override_pair<'a>(owner: Option<&'a str>, name: Option<&'a str>) -> Option<(&'a str, &'a str)> {
87 let owner = owner.filter(|v| !v.trim().is_empty())?;
88 let name = name.filter(|v| !v.trim().is_empty())?;
89 Some((owner, name))
90}
91
92pub fn resolve_github_slug_in(
97 override_owner: Option<&str>,
98 override_name: Option<&str>,
99 cwd: &Path,
100) -> Result<RepoSlug> {
101 if let Some((owner, name)) = override_pair(override_owner, override_name) {
102 return RepoSlug::validated(owner.to_string(), name.to_string());
103 }
104 let (owner, name) = detect_github_repo_in(cwd)?;
105 RepoSlug::validated(owner, name)
106}
107
108pub fn resolve_github_slug(
110 override_owner: Option<&str>,
111 override_name: Option<&str>,
112) -> Result<RepoSlug> {
113 resolve_github_slug_in(override_owner, override_name, &std::env::current_dir()?)
114}
115
116pub fn resolve_repo_slug_in(
123 override_owner: Option<&str>,
124 override_name: Option<&str>,
125 cwd: &Path,
126) -> Result<RepoSlug> {
127 if let Some((owner, name)) = override_pair(override_owner, override_name) {
128 return RepoSlug::validated(owner.to_string(), name.to_string());
129 }
130 let (owner, name) = detect_owner_repo_in(cwd)?;
131 RepoSlug::validated(owner, name)
132}
133
134pub fn resolve_repo_slug(
136 override_owner: Option<&str>,
137 override_name: Option<&str>,
138) -> Result<RepoSlug> {
139 resolve_repo_slug_in(override_owner, override_name, &std::env::current_dir()?)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::process::Command;
146
147 fn git(dir: &Path, args: &[&str]) {
148 let out = crate::test_helpers::output_with_spawn_retry(
149 || {
150 let mut cmd = Command::new("git");
151 cmd.args(args)
152 .current_dir(dir)
153 .env("GIT_TERMINAL_PROMPT", "0")
154 .env("LC_ALL", "C");
155 cmd
156 },
157 "git",
158 );
159 assert!(out.status.success(), "git {args:?} failed");
160 }
161
162 fn repo_with_origin(url: &str) -> tempfile::TempDir {
163 let tmp = tempfile::tempdir().unwrap();
164 git(tmp.path(), &["init", "-q"]);
165 git(tmp.path(), &["remote", "add", "origin", url]);
166 tmp
167 }
168
169 #[test]
170 fn override_wins_over_remote_without_touching_git() {
171 let tmp = tempfile::tempdir().unwrap();
174 let slug = resolve_github_slug_in(Some("cfg-owner"), Some("cfg-name"), tmp.path()).unwrap();
175 assert_eq!(slug.owner(), "cfg-owner");
176 assert_eq!(slug.name(), "cfg-name");
177 assert_eq!(slug.slug(), "cfg-owner/cfg-name");
178 }
179
180 #[test]
181 fn empty_override_falls_through_to_remote() {
182 let tmp = repo_with_origin("https://github.com/remote-owner/remote-repo.git");
183 let slug = resolve_github_slug_in(Some(""), Some(" "), tmp.path()).unwrap();
185 assert_eq!(slug.owner(), "remote-owner");
186 assert_eq!(slug.name(), "remote-repo");
187 }
188
189 #[test]
190 fn partial_override_falls_through_to_remote() {
191 let tmp = repo_with_origin("https://github.com/remote-owner/remote-repo.git");
192 let slug = resolve_github_slug_in(Some("cfg-owner"), None, tmp.path()).unwrap();
195 assert_eq!(slug.owner(), "remote-owner");
196 assert_eq!(slug.name(), "remote-repo");
197 }
198
199 #[test]
200 fn github_remote_derivation() {
201 let tmp = repo_with_origin("git@github.com:gh-owner/gh-repo.git");
202 let slug = resolve_github_slug_in(None, None, tmp.path()).unwrap();
203 assert_eq!(slug.slug(), "gh-owner/gh-repo");
204 }
205
206 #[test]
207 fn host_agnostic_derivation_preserves_nested_groups() {
208 let tmp = repo_with_origin("https://gitlab.com/group/subgroup/repo.git");
209 assert!(resolve_github_slug_in(None, None, tmp.path()).is_err());
211 let slug = resolve_repo_slug_in(None, None, tmp.path()).unwrap();
213 assert_eq!(slug.owner(), "group/subgroup");
214 assert_eq!(slug.name(), "repo");
215 }
216
217 #[test]
218 fn missing_remote_is_an_error_not_an_empty_slug() {
219 let tmp = tempfile::tempdir().unwrap();
220 git(tmp.path(), &["init", "-q"]);
221 assert!(resolve_github_slug_in(None, None, tmp.path()).is_err());
222 assert!(resolve_repo_slug_in(None, None, tmp.path()).is_err());
223 }
224}