1pub fn parse_remote_url_org_repo(url: &str) -> Option<(String, String)> {
47 let url = url.trim();
48 if url.is_empty() {
49 return None;
50 }
51
52 let path = if let Some(rest) = url
54 .strip_prefix("ssh://")
55 .or_else(|| url.strip_prefix("git://"))
56 {
57 rest.split_once('/')?.1
60 } else if url.contains("://") {
61 let after_scheme = url.split_once("://")?.1;
64 after_scheme.split_once('/')?.1
65 } else if let Some(colon_pos) = url.find(':') {
66 let before_colon = &url[..colon_pos];
69 if before_colon.contains('/') {
70 return None;
72 }
73 &url[colon_pos + 1..]
74 } else {
75 return None;
76 };
77
78 extract_org_repo_from_path(path)
79}
80
81fn extract_org_repo_from_path(path: &str) -> Option<(String, String)> {
86 let path = path.trim_start_matches('/');
87 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
88
89 if segments.len() < 2 {
90 return None;
91 }
92
93 let org = segments[segments.len() - 2];
94 let repo_raw = segments[segments.len() - 1];
95 let repo = repo_raw.strip_suffix(".git").unwrap_or(repo_raw);
96
97 if org.is_empty() || repo.is_empty() {
98 return None;
99 }
100
101 Some((org.to_string(), repo.to_string()))
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
111 fn parses_scp_ssh_url() {
112 let result = parse_remote_url_org_repo("git@github.com:withakay/ito.git");
113 assert_eq!(result, Some(("withakay".to_string(), "ito".to_string())));
114 }
115
116 #[test]
117 fn parses_https_url_with_git_suffix() {
118 let result = parse_remote_url_org_repo("https://github.com/withakay/ito.git");
119 assert_eq!(result, Some(("withakay".to_string(), "ito".to_string())));
120 }
121
122 #[test]
123 fn parses_https_url_without_git_suffix() {
124 let result = parse_remote_url_org_repo("https://github.com/withakay/ito");
125 assert_eq!(result, Some(("withakay".to_string(), "ito".to_string())));
126 }
127
128 #[test]
129 fn parses_ssh_with_explicit_port() {
130 let result = parse_remote_url_org_repo("ssh://git@github.com:22/withakay/ito.git");
131 assert_eq!(result, Some(("withakay".to_string(), "ito".to_string())));
132 }
133
134 #[test]
137 fn parses_gitlab_style_subgroup_takes_last_two_segments() {
138 let result = parse_remote_url_org_repo("https://gitlab.com/group/subgroup/repo.git");
140 assert_eq!(result, Some(("subgroup".to_string(), "repo".to_string())));
141 }
142
143 #[test]
144 fn parses_http_scheme() {
145 let result = parse_remote_url_org_repo("http://github.com/acme/widget.git");
146 assert_eq!(result, Some(("acme".to_string(), "widget".to_string())));
147 }
148
149 #[test]
150 fn parses_git_protocol_url() {
151 let result = parse_remote_url_org_repo("git://github.com/acme/widget.git");
152 assert_eq!(result, Some(("acme".to_string(), "widget".to_string())));
153 }
154
155 #[test]
156 fn strips_git_suffix_only_once() {
157 let result = parse_remote_url_org_repo("https://github.com/org/repo.git.git");
159 assert_eq!(result, Some(("org".to_string(), "repo.git".to_string())));
160 }
161
162 #[test]
163 fn handles_trailing_slash_in_https_url() {
164 let result = parse_remote_url_org_repo("https://github.com/withakay/ito/");
165 assert_eq!(result, Some(("withakay".to_string(), "ito".to_string())));
166 }
167
168 #[test]
169 fn handles_ssh_url_without_user() {
170 let result = parse_remote_url_org_repo("ssh://github.com/withakay/ito.git");
171 assert_eq!(result, Some(("withakay".to_string(), "ito".to_string())));
172 }
173
174 #[test]
177 fn returns_none_for_empty_string() {
178 assert_eq!(parse_remote_url_org_repo(""), None);
179 }
180
181 #[test]
182 fn returns_none_for_whitespace_only() {
183 assert_eq!(parse_remote_url_org_repo(" "), None);
184 }
185
186 #[test]
187 fn returns_none_for_single_path_component() {
188 assert_eq!(
189 parse_remote_url_org_repo("https://github.com/onlyone"),
190 None
191 );
192 }
193
194 #[test]
195 fn returns_none_for_no_path_after_host() {
196 assert_eq!(parse_remote_url_org_repo("https://github.com"), None);
197 assert_eq!(parse_remote_url_org_repo("https://github.com/"), None);
198 }
199
200 #[test]
201 fn returns_none_for_scp_url_with_single_component() {
202 assert_eq!(
203 parse_remote_url_org_repo("git@github.com:onlyone.git"),
204 None
205 );
206 }
207
208 #[test]
209 fn returns_none_for_bare_string_without_separator() {
210 assert_eq!(parse_remote_url_org_repo("notaurl"), None);
211 }
212}