1use std::path::{Path, PathBuf};
9
10use sley_config::GitConfig;
11use sley_config::remotes::{resolve_remote_fetch_url, resolve_remote_push_url};
12use sley_core::{GitError, Result};
13use sley_odb::repository_common_dir;
14use sley_transport::{RemoteTransport, RemoteUrl, parse_remote_url};
15
16use crate::{FetchSource, PushDestination, RemoteTransportKind};
17
18pub fn fetch_url(config: &GitConfig, remote: &str) -> String {
20 resolve_remote_fetch_url(config, remote)
21}
22
23pub fn push_url(config: &GitConfig, remote: &str) -> String {
25 resolve_remote_push_url(config, remote)
26}
27
28pub fn transport_kind_for_url(url: &str) -> Result<Option<RemoteTransportKind>> {
30 if url.ends_with(".bundle") {
31 return Ok(Some(RemoteTransportKind::Bundle));
32 }
33 Ok(match parse_remote_url(url)?.transport {
34 RemoteTransport::Http | RemoteTransport::Https => Some(RemoteTransportKind::Http),
35 RemoteTransport::Ssh | RemoteTransport::Ext => Some(RemoteTransportKind::Ssh),
36 RemoteTransport::Git => Some(RemoteTransportKind::Git),
37 RemoteTransport::Local | RemoteTransport::File => Some(RemoteTransportKind::Local),
38 })
39}
40
41pub fn fetch_source_for_url(url: &str, relative_base: &Path) -> Result<FetchSource> {
46 let parsed = parse_remote_url(url)?;
47 source_from_parsed(&parsed, relative_base).map(FetchSource::from_concrete)
48}
49
50pub fn push_destination_for_url(url: &str, relative_base: &Path) -> Result<PushDestination> {
52 let parsed = parse_remote_url(url)?;
53 source_from_parsed(&parsed, relative_base).map(PushDestination::from_concrete)
54}
55
56pub fn resolve_fetch_source(
58 config: &GitConfig,
59 remote: &str,
60 relative_base: &Path,
61) -> Result<(String, FetchSource)> {
62 let url = fetch_url(config, remote);
63 let source = fetch_source_for_url(&url, relative_base)?;
64 Ok((url, source))
65}
66
67pub fn resolve_push_destination(
69 config: &GitConfig,
70 remote: &str,
71 relative_base: &Path,
72) -> Result<(String, PushDestination)> {
73 let url = push_url(config, remote);
74 let destination = push_destination_for_url(&url, relative_base)?;
75 Ok((url, destination))
76}
77
78enum ConcreteRemote {
79 Network(RemoteUrl),
80 Local {
81 git_dir: PathBuf,
82 common_git_dir: PathBuf,
83 },
84}
85
86impl FetchSource {
87 fn from_concrete(source: ConcreteRemote) -> Self {
88 match source {
89 ConcreteRemote::Network(remote) => match remote.transport {
90 RemoteTransport::Http | RemoteTransport::Https => Self::Http(remote),
91 RemoteTransport::Ssh | RemoteTransport::Ext => Self::Ssh(remote),
92 RemoteTransport::Git => Self::Git {
93 remote,
94 protocol_v2: false,
95 },
96 RemoteTransport::Local | RemoteTransport::File => {
97 unreachable!("local remotes use FetchSource::Local")
98 }
99 },
100 ConcreteRemote::Local {
101 git_dir,
102 common_git_dir,
103 } => Self::Local {
104 git_dir,
105 common_git_dir,
106 },
107 }
108 }
109}
110
111impl PushDestination {
112 fn from_concrete(source: ConcreteRemote) -> Self {
113 match source {
114 ConcreteRemote::Network(remote) => match remote.transport {
115 RemoteTransport::Http | RemoteTransport::Https => Self::Http(remote),
116 RemoteTransport::Ssh | RemoteTransport::Ext => Self::Ssh(remote),
117 RemoteTransport::Git => Self::Git(remote),
118 RemoteTransport::Local | RemoteTransport::File => {
119 unreachable!("local remotes use PushDestination::Local")
120 }
121 },
122 ConcreteRemote::Local {
123 git_dir,
124 common_git_dir,
125 } => Self::Local {
126 git_dir,
127 common_git_dir,
128 },
129 }
130 }
131}
132
133fn source_from_parsed(parsed: &RemoteUrl, relative_base: &Path) -> Result<ConcreteRemote> {
134 match parsed.transport {
135 RemoteTransport::Http
136 | RemoteTransport::Https
137 | RemoteTransport::Ssh
138 | RemoteTransport::Ext
139 | RemoteTransport::Git => Ok(ConcreteRemote::Network(parsed.clone())),
140 RemoteTransport::Local | RemoteTransport::File => {
141 let repo_path = local_repository_path(parsed, relative_base)?;
142 let git_dir = discover_git_dir(&repo_path)?;
143 Ok(ConcreteRemote::Local {
144 common_git_dir: repository_common_dir(&git_dir),
145 git_dir,
146 })
147 }
148 }
149}
150
151fn local_repository_path(parsed: &RemoteUrl, relative_base: &Path) -> Result<PathBuf> {
152 Ok(match parsed.transport {
153 RemoteTransport::Local => {
154 let path = PathBuf::from(&parsed.path);
155 if path.is_absolute() {
156 path
157 } else {
158 relative_base.join(path)
159 }
160 }
161 RemoteTransport::File => PathBuf::from(&parsed.path),
162 _ => {
163 return Err(GitError::Unsupported("expected a local remote URL".into()));
164 }
165 })
166}
167
168fn discover_git_dir(start: &Path) -> Result<PathBuf> {
170 let absolute = if start.is_absolute() {
171 start.to_path_buf()
172 } else {
173 std::env::current_dir().map_err(GitError::from)?.join(start)
174 };
175 for candidate in absolute.ancestors() {
176 let dot_git = candidate.join(".git");
177 if dot_git.is_dir() {
178 return Ok(dot_git);
179 }
180 if dot_git.is_file()
181 && let Some(git_dir) = read_gitdir_link(&dot_git)?
182 && is_git_dir(&git_dir)
183 {
184 return Ok(git_dir);
185 }
186 if is_git_dir(candidate) {
187 return Ok(candidate.to_path_buf());
188 }
189 }
190 Err(GitError::repository_not_found(format!(
191 "not a git repository: {}",
192 start.display()
193 )))
194}
195
196fn is_git_dir(path: &Path) -> bool {
197 path.join("HEAD").is_file()
198 && (path.join("objects").is_dir() || path.join("commondir").is_file())
199}
200
201fn read_gitdir_link(path: &Path) -> Result<Option<PathBuf>> {
202 let contents = std::fs::read_to_string(path)?;
203 let Some(target) = contents.trim().strip_prefix("gitdir:") else {
204 return Ok(None);
205 };
206 let target = PathBuf::from(target.trim());
207 Ok(Some(if target.is_absolute() {
208 target
209 } else {
210 path.parent().unwrap_or_else(|| Path::new("")).join(target)
211 }))
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use sley_config::{ConfigEntry, ConfigSection, GitConfig};
218
219 #[test]
220 fn instead_of_rewrites_fetch_url() {
221 let config = GitConfig {
222 preamble: Vec::new(),
223 suffix: Vec::new(),
224 sections: vec![
225 ConfigSection::new(
226 "remote",
227 Some("origin".into()),
228 vec![ConfigEntry::new(
229 "url",
230 Some("git@github.com:org/repo.git".into()),
231 )],
232 ),
233 ConfigSection::new(
234 "url",
235 Some("https://github.com/".into()),
236 vec![ConfigEntry::new(
237 "insteadOf",
238 Some("git@github.com:".into()),
239 )],
240 ),
241 ],
242 };
243 assert_eq!(
244 fetch_url(&config, "origin"),
245 "https://github.com/org/repo.git"
246 );
247 }
248
249 #[test]
250 fn push_url_prefers_pushurl() {
251 let config = GitConfig {
252 preamble: Vec::new(),
253 suffix: Vec::new(),
254 sections: vec![ConfigSection::new(
255 "remote",
256 Some("origin".into()),
257 vec![
258 ConfigEntry::new("url", Some("https://fetch.example/x.git".into())),
259 ConfigEntry::new("pushurl", Some("https://push.example/x.git".into())),
260 ],
261 )],
262 };
263 assert_eq!(push_url(&config, "origin"), "https://push.example/x.git");
264 }
265
266 #[test]
267 fn git_scheme_routes_to_native_git_transport() {
268 let url = "git://127.0.0.1/repo.git";
269
270 assert_eq!(
271 transport_kind_for_url(url).expect("kind"),
272 Some(RemoteTransportKind::Git)
273 );
274 assert!(matches!(
275 fetch_source_for_url(url, Path::new(".")).expect("fetch source"),
276 FetchSource::Git { .. }
277 ));
278 assert!(matches!(
279 push_destination_for_url(url, Path::new(".")).expect("push destination"),
280 PushDestination::Git(_)
281 ));
282 }
283}