reference_query/core/
identity.rs1use std::fmt;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub enum RepoIdentity {
11 Remote(String),
13 Local(String),
15 Named(String),
17}
18
19impl RepoIdentity {
20 pub fn from_remote_url(url: &str) -> Option<RepoIdentity> {
30 normalize_remote(url).map(RepoIdentity::Remote)
31 }
32
33 pub fn local(path: &str) -> RepoIdentity {
35 RepoIdentity::Local(path.to_string())
36 }
37}
38
39impl fmt::Display for RepoIdentity {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 RepoIdentity::Remote(s) => f.write_str(s),
43 RepoIdentity::Local(p) => write!(f, "local:{p}"),
44 RepoIdentity::Named(n) => f.write_str(n),
45 }
46 }
47}
48
49fn normalize_remote(url: &str) -> Option<String> {
52 let url = url.trim();
53 if url.is_empty() {
54 return None;
55 }
56
57 let rest = if let Some(stripped) = strip_scheme(url) {
59 stripped
61 } else if let Some((host_part, path)) = url.split_once(':') {
62 let host = host_part.rsplit('@').next().unwrap_or(host_part);
64 return assemble(host, path);
65 } else {
66 return None;
67 };
68
69 let (authority, path) = rest.split_once('/')?;
70 let host_with_user = authority;
71 let host = host_with_user.rsplit('@').next().unwrap_or(host_with_user);
72 let host = host.split(':').next().unwrap_or(host); assemble(host, path)
74}
75
76fn strip_scheme(url: &str) -> Option<&str> {
78 for scheme in ["https://", "http://", "ssh://", "git://"] {
79 if let Some(rest) = url.strip_prefix(scheme) {
80 return Some(rest);
81 }
82 }
83 None
84}
85
86fn assemble(host: &str, path: &str) -> Option<String> {
87 let host = host.trim().trim_matches('/');
88 let path = path.trim().trim_matches('/');
89 let path = path.strip_suffix(".git").unwrap_or(path);
90 if host.is_empty() || path.is_empty() {
91 return None;
92 }
93 Some(format!("{host}/{path}"))
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn normalizes_scp_syntax() {
102 assert_eq!(
103 RepoIdentity::from_remote_url("git@github.com:dpep/rq.git"),
104 Some(RepoIdentity::Remote("github.com/dpep/rq".into()))
105 );
106 }
107
108 #[test]
109 fn normalizes_https() {
110 assert_eq!(
111 RepoIdentity::from_remote_url("https://github.com/dpep/rq.git"),
112 Some(RepoIdentity::Remote("github.com/dpep/rq".into()))
113 );
114 }
115
116 #[test]
117 fn normalizes_ssh_with_user_and_port() {
118 assert_eq!(
119 RepoIdentity::from_remote_url("ssh://git@github.com:22/dpep/rq"),
120 Some(RepoIdentity::Remote("github.com/dpep/rq".into()))
121 );
122 }
123
124 #[test]
125 fn forks_and_clones_share_identity() {
126 let a = RepoIdentity::from_remote_url("git@github.com:dpep/rq.git");
127 let b = RepoIdentity::from_remote_url("https://github.com/dpep/rq");
128 assert_eq!(a, b);
129 }
130
131 #[test]
132 fn empty_and_garbage_return_none() {
133 assert_eq!(RepoIdentity::from_remote_url(""), None);
134 assert_eq!(RepoIdentity::from_remote_url("not-a-url"), None);
135 }
136
137 #[test]
138 fn display_renders_each_variant() {
139 assert_eq!(
140 RepoIdentity::Remote("github.com/dpep/rq".into()).to_string(),
141 "github.com/dpep/rq"
142 );
143 assert_eq!(
144 RepoIdentity::local("/home/dpep/rq").to_string(),
145 "local:/home/dpep/rq"
146 );
147 assert_eq!(RepoIdentity::Named("work".into()).to_string(), "work");
148 }
149}