1use std::path::Path;
23
24use sley_config::GitConfig;
25use sley_core::{ObjectFormat, Result};
26use sley_transport::GitCredential;
27
28mod credentials;
29pub use credentials::{
30 CredentialHelperProvider, credential_fill, credential_request_for_url, credential_store,
31 http_credential_host, http_protocol_name, http_url_credential,
32};
33
34#[cfg(feature = "http")]
35mod http;
36#[cfg(feature = "http")]
37pub use http::{
38 HttpFetchPackRequest, HttpServiceAdvertisements, http_advertised_refs,
39 http_authorization_headers, http_check_status, http_protocol_v2_fetch_response,
40 http_send_with_auth, http_service_advertisements, http_upload_pack_advertisements,
41 http_upload_pack_fetch_response, http_upload_pack_shallow_fetch_response,
42 http_validate_content_type, install_fetch_pack_via_http_protocol_v2_fetch,
43 install_fetch_pack_via_http_upload_pack, new_http_client, remote_url_is_http,
44};
45
46mod ssh;
47pub use ssh::{
48 SshFetchPackRequest, install_fetch_pack_via_ssh_upload_pack, ssh_program,
49 ssh_upload_pack_advertisements, ssh_upload_pack_fetch_response,
50 ssh_upload_pack_shallow_fetch_response,
51};
52
53mod git;
54pub use git::{
55 GitFetchPackRequest, git_upload_pack_advertisements, install_fetch_pack_via_git_upload_pack,
56};
57
58mod local;
59pub use local::{
60 INFINITE_DEPTH, LocalDeepenPlan, attach_receive_pack_capabilities,
61 attach_upload_pack_capabilities, compute_local_deepen, compute_local_deepen_by_rev_list,
62 install_fetch_pack_via_local_upload_pack, local_fetch_advertisements, local_have_oids,
63 receive_pack_features, receive_pack_into_local_repository,
64 receive_pack_request_uses_push_options, upload_pack_features,
65 upload_pack_from_local_repository, upload_pack_request_uses_sideband,
66 upload_pack_sideband_response,
67};
68
69mod fetch;
70pub use fetch::{
71 FetchOptions, FetchOutcome, FetchRequest, FetchServices, FetchSource, PrunedRef,
72 append_reachable_auto_follow_tags, apply_configured_fetch_prune_option,
73 apply_configured_remote_tag_option, fetch, fetch_head_source_description,
74 fetch_refspec_excludes, fetch_refspecs_for_source, mark_tag_refspec_updates_not_for_merge,
75 order_bundle_fetch_all_tags_updates, prune_remote_tracking_refs_from_advertisements,
76 retain_missing_auto_follow_tags, write_default_fetch_head, write_fetch_head,
77 write_fetch_head_records,
78};
79
80mod pack;
81pub use pack::{
82 PushPackRequest, build_push_packfile, build_receive_pack_body,
83 remote_advertisement_tips_known_to_local,
84};
85
86mod push;
87pub use push::{
88 PushAction, PushActionPlan, PushActionRequest, PushCommand, PushDestination, PushOptions,
89 PushOutcome, PushPlan, PushRequest, PushServices, execute_push_action_plan, execute_push_plan,
90 local_push_source_refs, normalize_push_refname, normalize_push_refspec, plan_push,
91 plan_push_actions, push, push_actions, reject_non_fast_forward_pushes,
92 validate_receive_pack_report,
93};
94
95mod ls_remote;
96pub use ls_remote::{LsRemoteFilter, LsRemoteRecord, LsRemoteSource, ls_remote};
97
98mod clone;
99pub use clone::{CloneOptions, CloneOutcome, CloneRequest, CloneServices, CloneSource, clone};
100
101mod bundle;
102pub use bundle::{FetchBundleRequest, fetch_bundle};
103
104mod shallow;
105pub use shallow::{apply_shallow_info, read_shallow, write_shallow};
106
107mod capabilities;
108pub use capabilities::{
109 BUNDLE_FETCH_SUPPORTED, HTTP_PROTOCOL_V2_FETCH, RemoteTransportKind, SSH_CLONE_SUPPORTED,
110 THIN_PACK_PUSH_SUPPORTED, TransportCapabilities,
111};
112
113mod resolve;
114pub use resolve::{
115 fetch_source_for_url, fetch_url, push_destination_for_url, push_url, resolve_fetch_source,
116 resolve_push_destination, transport_kind_for_url,
117};
118
119pub fn object_format_for_git_dir(common_git_dir: &Path) -> Result<ObjectFormat> {
125 let Ok(config) = GitConfig::read(common_git_dir.join("config")) else {
126 return Ok(ObjectFormat::Sha1);
127 };
128 config.repository_object_format()
129}
130
131pub trait CredentialProvider {
140 fn fill(&mut self, request: GitCredential) -> Result<Option<GitCredential>>;
143
144 fn approve(&mut self, _credential: &GitCredential) -> Result<()> {
146 Ok(())
147 }
148
149 fn reject(&mut self, _credential: &GitCredential) -> Result<()> {
151 Ok(())
152 }
153}
154
155#[derive(Debug, Default, Clone, Copy)]
159pub struct NoCredentials;
160
161impl CredentialProvider for NoCredentials {
162 fn fill(&mut self, _request: GitCredential) -> Result<Option<GitCredential>> {
163 Ok(None)
164 }
165}
166
167pub trait ProgressSink {
172 fn message(&mut self, _message: &str) {}
174}
175
176#[derive(Debug, Default, Clone, Copy)]
178pub struct SilentProgress;
179
180impl ProgressSink for SilentProgress {}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::fs;
186 use std::path::{Path, PathBuf};
187 use std::sync::atomic::{AtomicU64, Ordering};
188
189 use sley_config::{ConfigEntry, ConfigSection};
190 use sley_formats::RepositoryLayout;
191 use sley_object::{Commit, EncodedObject, ObjectType, Tree};
192 use sley_odb::{FileObjectDatabase, ObjectWriter};
193 use sley_refs::{FileRefStore, RefTarget, RefUpdate};
194 use sley_transport::{RemoteUrl, parse_remote_url};
195
196 #[test]
197 fn no_credentials_never_fills() {
198 let mut provider = NoCredentials;
199 let request = GitCredential::default();
200 assert!(
201 provider
202 .fill(request)
203 .expect("test operation should succeed")
204 .is_none()
205 );
206 }
207
208 #[test]
209 fn silent_progress_accepts_messages() {
210 let mut progress = SilentProgress;
211 progress.message("Cloning into 'x'...");
212 }
213
214 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
215
216 fn live_env(name: &str) -> Option<String> {
217 match std::env::var(name) {
218 Ok(value) if !value.is_empty() => Some(value),
219 _ => None,
220 }
221 }
222
223 fn live_repo(name: &str) -> PathBuf {
224 let dir = std::env::temp_dir().join(format!(
225 "sley-remote-live-{name}-{}-{}",
226 std::process::id(),
227 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
228 ));
229 let _ = fs::remove_dir_all(&dir);
230 RepositoryLayout::init_at(&dir, ObjectFormat::Sha1, false)
231 .expect("live test repository should initialize");
232 dir.join(".git")
233 }
234
235 fn remote_config(url: &str) -> GitConfig {
236 GitConfig {
237 sections: vec![ConfigSection::new(
238 "remote",
239 Some("origin".into()),
240 vec![
241 ConfigEntry::new("url", Some(url.into())),
242 ConfigEntry::new("fetch", Some("+refs/heads/*:refs/remotes/origin/*".into())),
243 ],
244 )],
245 ..GitConfig::default()
246 }
247 }
248
249 fn fetch_options(depth: Option<u32>) -> FetchOptions {
250 FetchOptions {
251 quiet: true,
252 auto_follow_tags: false,
253 fetch_all_tags: false,
254 prune: false,
255 dry_run: false,
256 append: false,
257 write_fetch_head: true,
258 tag_option_explicit: true,
259 prune_option_explicit: true,
260 depth,
261 merge_srcs: Vec::new(),
262 filter: None,
263 cloning: false,
264 update_shallow: false,
265 deepen_relative: false,
266 deepen_since: None,
267 deepen_not: Vec::new(),
268 }
269 }
270
271 fn write_live_commit(git_dir: &Path, branch: &str) {
272 let format = ObjectFormat::Sha1;
273 let db = FileObjectDatabase::from_git_dir(git_dir, format);
274 let tree = db
275 .write_object(EncodedObject::new(
276 ObjectType::Tree,
277 Tree { entries: vec![] }.write(),
278 ))
279 .expect("live commit tree should write");
280 let timestamp = 1 + TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
281 let identity =
282 format!("Sley Remote Live <sley@example.invalid> {timestamp} +0000").into_bytes();
283 let oid = db
284 .write_object(EncodedObject::new(
285 ObjectType::Commit,
286 Commit {
287 tree,
288 parents: Vec::new(),
289 author: identity.clone(),
290 committer: identity,
291 encoding: None,
292 message: format!("sley remote live {branch}\n").into_bytes(),
293 }
294 .write(),
295 ))
296 .expect("live commit should write");
297 let store = FileRefStore::new(git_dir, format);
298 let mut tx = store.transaction();
299 tx.update(RefUpdate {
300 name: format!("refs/heads/{branch}"),
301 expected: None,
302 new: RefTarget::Direct(oid),
303 reflog: None,
304 });
305 tx.update(RefUpdate {
306 name: "HEAD".into(),
307 expected: None,
308 new: RefTarget::Symbolic(format!("refs/heads/{branch}")),
309 reflog: None,
310 });
311 tx.commit().expect("live refs should update");
312 }
313
314 struct EnvCredentials {
315 username: String,
316 password: String,
317 }
318
319 impl CredentialProvider for EnvCredentials {
320 fn fill(&mut self, mut request: GitCredential) -> Result<Option<GitCredential>> {
321 request.username = Some(self.username.clone());
322 request.password = Some(self.password.clone());
323 Ok(Some(request))
324 }
325 }
326
327 fn live_fetch(
328 url_var: &str,
329 branch_var: &str,
330 source: FetchSource,
331 credentials: &mut dyn CredentialProvider,
332 depth: Option<u32>,
333 ) {
334 let Some(url) = live_env(url_var) else {
335 return;
336 };
337 let branch = live_env(branch_var).unwrap_or_else(|| "main".into());
338 let local = live_repo(url_var);
339 let refspec = format!("refs/heads/{branch}:refs/remotes/origin/{branch}");
340 let config = remote_config(&url);
341 let options = fetch_options(depth);
342 let mut progress = SilentProgress;
343
344 let outcome = fetch(
345 FetchRequest {
346 git_dir: &local,
347 format: ObjectFormat::Sha1,
348 config: &config,
349 remote_name: "origin",
350 source: &source,
351 refspecs: &[refspec],
352 options: &options,
353 },
354 FetchServices {
355 credentials,
356 progress: &mut progress,
357 },
358 )
359 .expect("live fetch should succeed");
360
361 assert!(!outcome.ref_updates.is_empty());
362 if depth.is_some() {
363 assert!(
364 local.join("shallow").exists(),
365 "shallow fetch should write .git/shallow"
366 );
367 }
368 }
369
370 fn live_push(
371 url_var: &str,
372 branch_prefix_var: &str,
373 destination: PushDestination,
374 credentials: &mut dyn CredentialProvider,
375 ) {
376 let Some(_) = live_env(url_var) else {
377 return;
378 };
379 let branch_prefix =
380 live_env(branch_prefix_var).unwrap_or_else(|| "sley-remote-live".into());
381 let branch = format!(
382 "{branch_prefix}-{}-{}",
383 std::process::id(),
384 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
385 );
386 let local = live_repo(url_var);
387 write_live_commit(&local, &branch);
388 let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
389 let options = PushOptions {
390 quiet: true,
391 force: false,
392 };
393 let mut progress = SilentProgress;
394
395 let outcome = push(
396 PushRequest {
397 git_dir: &local,
398 common_git_dir: &local,
399 format: ObjectFormat::Sha1,
400 config: &GitConfig::default(),
401 remote: "origin",
402 destination: &destination,
403 refspecs: &[refspec],
404 options: &options,
405 },
406 PushServices {
407 credentials,
408 progress: &mut progress,
409 },
410 )
411 .expect("live push should succeed");
412
413 assert_eq!(outcome.commands.len(), 1);
414 }
415
416 #[test]
417 fn live_github_https_public_fetch() {
418 let Some(url) = live_env("SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_URL") else {
419 return;
420 };
421 let remote = parse_remote_url(&url).expect("live HTTPS URL should parse");
422 let mut credentials = NoCredentials;
423 live_fetch(
424 "SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_URL",
425 "SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_BRANCH",
426 FetchSource::Http(remote),
427 &mut credentials,
428 None,
429 );
430 }
431
432 #[test]
433 fn live_private_https_auth_fetch_uses_credential_provider() {
434 let (Some(url), Some(username), Some(password)) = (
435 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_URL"),
436 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_USERNAME"),
437 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_PASSWORD"),
438 ) else {
439 return;
440 };
441 let remote = parse_remote_url(&url).expect("live private HTTPS URL should parse");
442 let mut credentials = EnvCredentials { username, password };
443 live_fetch(
444 "SLEY_REMOTE_LIVE_PRIVATE_HTTPS_URL",
445 "SLEY_REMOTE_LIVE_PRIVATE_HTTPS_BRANCH",
446 FetchSource::Http(remote),
447 &mut credentials,
448 None,
449 );
450 }
451
452 #[test]
453 fn live_https_push() {
454 let Some(url) = live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_URL") else {
455 return;
456 };
457 let remote = parse_remote_url(&url).expect("live HTTPS push URL should parse");
458 let mut no_credentials;
459 let mut env_credentials;
460 let credentials: &mut dyn CredentialProvider = match (
461 live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_USERNAME"),
462 live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_PASSWORD"),
463 ) {
464 (Some(username), Some(password)) => {
465 env_credentials = EnvCredentials { username, password };
466 &mut env_credentials
467 }
468 _ => {
469 no_credentials = NoCredentials;
470 &mut no_credentials
471 }
472 };
473 live_push(
474 "SLEY_REMOTE_LIVE_HTTPS_PUSH_URL",
475 "SLEY_REMOTE_LIVE_HTTPS_PUSH_BRANCH_PREFIX",
476 PushDestination::Http(remote),
477 credentials,
478 );
479 }
480
481 #[test]
482 fn live_ssh_fetch() {
483 let Some(url) = live_env("SLEY_REMOTE_LIVE_SSH_FETCH_URL") else {
484 return;
485 };
486 let remote = parse_remote_url(&url).expect("live SSH fetch URL should parse");
487 let mut credentials = NoCredentials;
488 live_fetch(
489 "SLEY_REMOTE_LIVE_SSH_FETCH_URL",
490 "SLEY_REMOTE_LIVE_SSH_FETCH_BRANCH",
491 FetchSource::Ssh(remote),
492 &mut credentials,
493 None,
494 );
495 }
496
497 #[test]
498 fn live_ssh_push() {
499 let Some(url) = live_env("SLEY_REMOTE_LIVE_SSH_PUSH_URL") else {
500 return;
501 };
502 let remote = parse_remote_url(&url).expect("live SSH push URL should parse");
503 let mut credentials = NoCredentials;
504 live_push(
505 "SLEY_REMOTE_LIVE_SSH_PUSH_URL",
506 "SLEY_REMOTE_LIVE_SSH_PUSH_BRANCH_PREFIX",
507 PushDestination::Ssh(remote),
508 &mut credentials,
509 );
510 }
511
512 #[test]
513 fn live_shallow_https_fetch_and_clone() {
514 let Some(url) = live_env("SLEY_REMOTE_LIVE_SHALLOW_HTTPS_URL") else {
515 return;
516 };
517 let branch =
518 live_env("SLEY_REMOTE_LIVE_SHALLOW_HTTPS_BRANCH").unwrap_or_else(|| "main".into());
519 let remote = parse_remote_url(&url).expect("live shallow HTTPS URL should parse");
520 let mut credentials = NoCredentials;
521 live_fetch(
522 "SLEY_REMOTE_LIVE_SHALLOW_HTTPS_URL",
523 "SLEY_REMOTE_LIVE_SHALLOW_HTTPS_BRANCH",
524 FetchSource::Http(remote.clone()),
525 &mut credentials,
526 Some(1),
527 );
528
529 let destination = std::env::temp_dir().join(format!(
530 "sley-remote-live-clone-{}-{}",
531 std::process::id(),
532 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
533 ));
534 let _ = fs::remove_dir_all(&destination);
535 let config = remote_config(&url);
536 let mut configure = |_git_dir: &Path| Ok(config.clone());
537 let mut configure_branch = |_git_dir: &Path, _branch: &str| Ok(config.clone());
538 let options = CloneOptions {
539 origin: "origin",
540 checkout_branch: &branch,
541 remote_head_branch: &branch,
542 single_branch: true,
543 depth: Some(1),
544 deepen_since: None,
545 deepen_not: Vec::new(),
546 committer: b"Sley Remote Live <sley@example.invalid> 1 +0000".to_vec(),
547 detached_head: None,
548 filter: None,
549 branch_explicit: true,
552 };
553 let mut clone_credentials = NoCredentials;
554 let mut progress = SilentProgress;
555
556 let outcome = clone(
557 CloneRequest {
558 destination: &destination,
559 format: ObjectFormat::Sha1,
560 source: &CloneSource::Http(RemoteUrl { ..remote }),
561 options: &options,
562 },
563 CloneServices {
564 configure: &mut configure,
565 configure_branch: &mut configure_branch,
566 credentials: &mut clone_credentials,
567 progress: &mut progress,
568 },
569 )
570 .expect("live shallow HTTPS clone should succeed");
571
572 assert!(outcome.git_dir.join("shallow").exists());
573 }
574}