1use std::path::Path;
23
24use sley_config::GitConfig;
25use sley_core::{ObjectFormat, Result};
26use sley_transport::GitCredential;
27
28mod install;
29pub use install::{
30 install_protocol_v2_fetch_promisor_response_from_reader,
31 install_protocol_v2_fetch_response_from_reader,
32 install_upload_pack_packfile_promisor_response_from_reader,
33 install_upload_pack_packfile_response_from_reader,
34 install_upload_pack_raw_promisor_response_from_reader,
35 install_upload_pack_raw_response_from_reader,
36 install_upload_pack_shallow_packfile_promisor_response_from_reader,
37 install_upload_pack_shallow_packfile_response_from_reader,
38 install_upload_pack_shallow_raw_promisor_response_from_reader,
39 install_upload_pack_shallow_raw_response_from_reader,
40 shallow_info_from_protocol_v2_fetch_header,
41};
42
43mod credentials;
44pub use credentials::{
45 CredentialHelperProvider, credential_fill, credential_request_for_url, credential_store,
46 http_credential_host, http_protocol_name, http_url_credential,
47};
48
49#[cfg(feature = "http")]
50mod http;
51#[cfg(feature = "http")]
52pub use http::{
53 HttpFetchPackRequest, HttpServiceAdvertisements, http_advertised_refs,
54 http_authorization_headers, http_check_status, http_protocol_v2_fetch_response,
55 http_send_with_auth, http_service_advertisements, http_upload_pack_advertisements,
56 http_upload_pack_features, http_validate_content_type,
57 install_fetch_pack_via_http_protocol_v2_fetch, install_fetch_pack_via_http_upload_pack,
58 new_http_client, HttpOperationBatch,
59 remote_url_is_http,
60};
61#[cfg(feature = "http")]
66pub use sley_transport::{HttpClient, HttpResponse, UreqHttpClient};
67
68mod ssh;
69pub use ssh::{
70 SshFetchPackRequest, SshTransportOptions, install_fetch_pack_via_ssh_upload_pack, ssh_program,
71 ssh_transport_options_from_config, ssh_upload_pack_advertisements,
72 ssh_upload_pack_advertisements_with_options,
73};
74
75mod git;
76mod git_proxy;
77pub use git::{
78 GitFetchPackRequest, git_upload_pack_advertisements,
79 git_upload_pack_advertisements_with_protocol, install_fetch_pack_via_git_upload_pack,
80};
81
82mod proc_receive;
83mod receive_hooks;
84pub use receive_hooks::{run_pre_receive, run_update_hooks};
85mod receive_pack_server;
86
87pub use proc_receive::{
88 ProcReceiveRefPattern, ProcReceiveReport, ReceivePackCommandState, parse_proc_receive_refs,
89 proc_receive_ref_matches,
90};
91pub use receive_pack_server::{
92 ReceivePackServerOptions, ReceivePackServerOutcome, ReceivePackServerReport,
93 ReceivePackServerRequest, flush_receive_pack_sideband, receive_pack_server_report_v1,
94 request_uses_sideband, run_receive_pack_post_hooks, serve_receive_pack,
95 write_receive_pack_server_report, write_receive_pack_sideband_stderr,
96};
97
98mod local;
99pub use local::{
100 INFINITE_DEPTH, LocalDeepenPlan, attach_receive_pack_capabilities,
101 attach_upload_pack_capabilities, compute_local_deepen, compute_local_deepen_by_rev_list,
102 install_fetch_pack_via_local_upload_pack, local_fetch_advertisements, local_have_oids,
103 receive_pack_features, receive_pack_into_local_repository,
104 receive_pack_request_uses_push_options, receive_pack_stream_into_local_repository,
105 serve_upload_pack_v2, serve_upload_pack_v2_with_config, upload_pack_features,
106 upload_pack_from_local_repository, upload_pack_request_uses_sideband,
107 upload_pack_sideband_response,
108};
109
110mod filter;
111pub use filter::{pack_filter_from_spec, pack_filter_from_spec_for_clone};
112
113mod fetch;
114pub use fetch::{
115 FetchOptions, FetchOutcome, FetchRequest, FetchServices, FetchSource, PruneRefsInput,
116 PrunedRef, append_reachable_auto_follow_tags, apply_configured_fetch_prune_option,
117 apply_configured_partial_clone_filter, apply_configured_remote_tag_option, fetch,
118 fetch_head_source_description,
119 fetch_refspec_excludes, fetch_refspecs_for_source, mark_tag_refspec_updates_not_for_merge,
120 order_bundle_fetch_all_tags_updates, prune_refs_from_advertisements,
121 retain_missing_auto_follow_tags, write_default_fetch_head, write_fetch_head,
122 write_fetch_head_records,
123};
124#[cfg(feature = "http")]
125pub use fetch::fetch_with_http_client;
126
127mod pack;
128pub use pack::{
129 PushPackRequest, build_push_packfile, build_receive_pack_body,
130 remote_advertisement_tips_known_to_local,
131};
132
133mod push;
134pub use push::{
135 PushAction, PushActionPlan, PushActionRequest, PushCommand, PushDestination, PushOptions,
136 PushOutcome, PushPlan, PushQuarantine, PushRefStatus, PushReportRef, PushReportRequest,
137 PushRequest, PushServices, PushStatusReport, PushThinMode, ReceivePackPushReport,
138 apply_receive_pack_report_to_push_refs, execute_push_action_plan, execute_push_plan,
139 local_push_source_refs, normalize_push_refname, normalize_push_refspec, plan_push,
140 plan_push_actions, push, push_actions, push_local_uses_receive_pack_server,
141 push_local_with_report, run_local_push_post_hooks, push_url_for_display,
142 read_receive_pack_push_report, reject_non_fast_forward_pushes, stage_local_push_quarantine,
143 validate_receive_pack_report, validate_receive_pack_unpack,
144};
145
146mod ls_remote;
147pub use ls_remote::{LsRemoteFilter, LsRemoteRecord, LsRemoteSource, ls_remote};
148
149mod clone;
150pub use clone::{CloneOptions, CloneOutcome, CloneRequest, CloneServices, CloneSource, clone};
151#[cfg(feature = "http")]
152pub use clone::clone_with_http_client;
153
154mod bundle;
155pub use bundle::{FetchBundleRequest, fetch_bundle};
156mod bundle_uri;
157pub use bundle_uri::{
158 BundleUriEntry, BundleUriList, bundle_uri_fetch_order, handshake_advertises_bundle_uri,
159 http_remote_bundle_uri_list, parse_bundle_uri_line, prefetch_advertised_bundle_uris,
160 transfer_bundle_uri_enabled,
161};
162
163mod shallow;
164pub use shallow::{apply_shallow_info, read_shallow, write_shallow};
165
166mod capabilities;
167pub use capabilities::{RemoteTransportKind, TransportCapabilities};
168
169mod protocol;
170pub use protocol::{
171 TransportPolicyError, check_transport_allowed, is_transport_allowed,
172 transport_scheme_for_remote, transport_scheme_for_url,
173};
174
175mod resolve;
176pub use resolve::{
177 fetch_source_for_url, fetch_url, push_destination_for_url, push_url, resolve_fetch_source,
178 resolve_push_destination, transport_kind_for_url,
179};
180
181pub fn object_format_for_git_dir(common_git_dir: &Path) -> Result<ObjectFormat> {
187 let Ok(config) = GitConfig::read(common_git_dir.join("config")) else {
188 return Ok(ObjectFormat::Sha1);
189 };
190 config.repository_object_format()
191}
192
193pub trait CredentialProvider {
202 fn fill(&mut self, request: GitCredential) -> Result<Option<GitCredential>>;
205
206 fn approve(&mut self, _credential: &GitCredential) -> Result<()> {
208 Ok(())
209 }
210
211 fn reject(&mut self, _credential: &GitCredential) -> Result<()> {
213 Ok(())
214 }
215}
216
217#[derive(Debug, Default, Clone, Copy)]
221pub struct NoCredentials;
222
223impl CredentialProvider for NoCredentials {
224 fn fill(&mut self, _request: GitCredential) -> Result<Option<GitCredential>> {
225 Ok(None)
226 }
227}
228
229#[derive(Debug, Clone, Copy, Default)]
232pub struct TransferProgress {
233 pub received_bytes: u64,
236 pub received_objects: u64,
238 pub total_objects: Option<u64>,
242 pub indexed_deltas: u64,
244}
245
246pub trait ProgressSink {
251 fn message(&mut self, _message: &str) {}
253 fn transfer(&mut self, _progress: TransferProgress) {}
255}
256
257#[derive(Debug, Default, Clone, Copy)]
259pub struct SilentProgress;
260
261impl ProgressSink for SilentProgress {}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use std::fs;
267 use std::path::{Path, PathBuf};
268 use std::sync::atomic::{AtomicU64, Ordering};
269
270 use sley_config::{ConfigEntry, ConfigSection};
271 use sley_formats::RepositoryLayout;
272 use sley_object::{Commit, EncodedObject, ObjectType, Tree};
273 use sley_odb::{FileObjectDatabase, ObjectWriter};
274 use sley_refs::{FileRefStore, RefTarget, RefUpdate};
275 use sley_transport::{RemoteUrl, parse_remote_url};
276
277 #[test]
278 fn no_credentials_never_fills() {
279 let mut provider = NoCredentials;
280 let request = GitCredential::default();
281 assert!(
282 provider
283 .fill(request)
284 .expect("test operation should succeed")
285 .is_none()
286 );
287 }
288
289 #[test]
290 fn silent_progress_accepts_messages() {
291 let mut progress = SilentProgress;
292 progress.message("Cloning into 'x'...");
293 }
294
295 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
296
297 fn live_env(name: &str) -> Option<String> {
298 match std::env::var(name) {
299 Ok(value) if !value.is_empty() => Some(value),
300 _ => None,
301 }
302 }
303
304 fn live_repo(name: &str) -> PathBuf {
305 let dir = std::env::temp_dir().join(format!(
306 "sley-remote-live-{name}-{}-{}",
307 std::process::id(),
308 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
309 ));
310 let _ = fs::remove_dir_all(&dir);
311 RepositoryLayout::init_at(&dir, ObjectFormat::Sha1, false)
312 .expect("live test repository should initialize");
313 dir.join(".git")
314 }
315
316 fn remote_config(url: &str) -> GitConfig {
317 GitConfig {
318 sections: vec![ConfigSection::new(
319 "remote",
320 Some("origin".into()),
321 vec![
322 ConfigEntry::new("url", Some(url.into())),
323 ConfigEntry::new("fetch", Some("+refs/heads/*:refs/remotes/origin/*".into())),
324 ],
325 )],
326 ..GitConfig::default()
327 }
328 }
329
330 fn fetch_options(depth: Option<u32>) -> FetchOptions {
331 FetchOptions {
332 quiet: true,
333 auto_follow_tags: false,
334 fetch_all_tags: false,
335 prune: false,
336 prune_tags: false,
337 dry_run: false,
338 force: false,
339 append: false,
340 write_fetch_head: true,
341 tag_option_explicit: true,
342 prune_option_explicit: true,
343 prune_tags_option_explicit: true,
344 refmap: None,
345 depth,
346 merge_srcs: Vec::new(),
347 filter: None,
348 refetch: false,
349 cloning: false,
350 record_promisor_refs: true,
351 update_shallow: false,
352 reject_shallow: false,
353 deepen_relative: false,
354 update_head_ok: false,
355 deepen_since: None,
356 deepen_not: Vec::new(),
357 ssh_options: None,
358 atomic: false,
359 negotiation_restrict: None,
360 negotiation_include: None,
361 }
362 }
363
364 fn write_live_commit(git_dir: &Path, branch: &str) {
365 let format = ObjectFormat::Sha1;
366 let db = FileObjectDatabase::from_git_dir(git_dir, format);
367 let tree = db
368 .write_object(EncodedObject::new(
369 ObjectType::Tree,
370 Tree { entries: vec![] }.write(),
371 ))
372 .expect("live commit tree should write");
373 let timestamp = 1 + TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
374 let identity =
375 format!("Sley Remote Live <sley@example.invalid> {timestamp} +0000").into_bytes();
376 let oid = db
377 .write_object(EncodedObject::new(
378 ObjectType::Commit,
379 Commit {
380 tree,
381 parents: Vec::new(),
382 author: identity.clone(),
383 committer: identity,
384 encoding: None,
385 message: format!("sley remote live {branch}\n").into_bytes(),
386 }
387 .write(),
388 ))
389 .expect("live commit should write");
390 let store = FileRefStore::new(git_dir, format);
391 let mut tx = store.transaction();
392 tx.update(RefUpdate {
393 name: format!("refs/heads/{branch}"),
394 expected: None,
395 new: RefTarget::Direct(oid),
396 reflog: None,
397 });
398 tx.update(RefUpdate {
399 name: "HEAD".into(),
400 expected: None,
401 new: RefTarget::Symbolic(format!("refs/heads/{branch}")),
402 reflog: None,
403 });
404 tx.commit().expect("live refs should update");
405 }
406
407 struct EnvCredentials {
408 username: String,
409 password: String,
410 }
411
412 impl CredentialProvider for EnvCredentials {
413 fn fill(&mut self, mut request: GitCredential) -> Result<Option<GitCredential>> {
414 request.username = Some(self.username.clone());
415 request.password = Some(self.password.clone());
416 Ok(Some(request))
417 }
418 }
419
420 fn live_fetch(
421 url_var: &str,
422 branch_var: &str,
423 source: FetchSource,
424 credentials: &mut dyn CredentialProvider,
425 depth: Option<u32>,
426 ) {
427 let Some(url) = live_env(url_var) else {
428 return;
429 };
430 let branch = live_env(branch_var).unwrap_or_else(|| "main".into());
431 let local = live_repo(url_var);
432 let refspec = format!("refs/heads/{branch}:refs/remotes/origin/{branch}");
433 let config = remote_config(&url);
434 let options = fetch_options(depth);
435 let mut progress = SilentProgress;
436
437 let outcome = fetch(
438 FetchRequest {
439 git_dir: &local,
440 format: ObjectFormat::Sha1,
441 config: &config,
442 remote_name: "origin",
443 source: &source,
444 refspecs: &[refspec],
445 options: &options,
446 },
447 FetchServices {
448 credentials,
449 progress: &mut progress,
450 ref_hook: None,
451 },
452 )
453 .expect("live fetch should succeed");
454
455 assert!(!outcome.ref_updates.is_empty());
456 if depth.is_some() {
457 assert!(
458 local.join("shallow").exists(),
459 "shallow fetch should write .git/shallow"
460 );
461 }
462 }
463
464 fn live_push(
465 url_var: &str,
466 branch_prefix_var: &str,
467 destination: PushDestination,
468 credentials: &mut dyn CredentialProvider,
469 ) {
470 let Some(_) = live_env(url_var) else {
471 return;
472 };
473 let branch_prefix =
474 live_env(branch_prefix_var).unwrap_or_else(|| "sley-remote-live".into());
475 let branch = format!(
476 "{branch_prefix}-{}-{}",
477 std::process::id(),
478 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
479 );
480 let local = live_repo(url_var);
481 write_live_commit(&local, &branch);
482 let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
483 let options = PushOptions {
484 quiet: true,
485 force: false,
486 thin: PushThinMode::Auto,
487 };
488 let mut progress = SilentProgress;
489
490 let outcome = push(
491 PushRequest {
492 git_dir: &local,
493 common_git_dir: &local,
494 format: ObjectFormat::Sha1,
495 config: &GitConfig::default(),
496 remote: "origin",
497 destination: &destination,
498 refspecs: &[refspec],
499 options: &options,
500 },
501 PushServices {
502 credentials,
503 progress: &mut progress,
504 },
505 )
506 .expect("live push should succeed");
507
508 assert_eq!(outcome.commands.len(), 1);
509 }
510
511 #[test]
512 fn live_github_https_public_fetch() {
513 let Some(url) = live_env("SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_URL") else {
514 return;
515 };
516 let remote = parse_remote_url(&url).expect("live HTTPS URL should parse");
517 let mut credentials = NoCredentials;
518 live_fetch(
519 "SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_URL",
520 "SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_BRANCH",
521 FetchSource::Http(remote),
522 &mut credentials,
523 None,
524 );
525 }
526
527 #[test]
528 fn live_private_https_auth_fetch_uses_credential_provider() {
529 let (Some(url), Some(username), Some(password)) = (
530 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_URL"),
531 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_USERNAME"),
532 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_PASSWORD"),
533 ) else {
534 return;
535 };
536 let remote = parse_remote_url(&url).expect("live private HTTPS URL should parse");
537 let mut credentials = EnvCredentials { username, password };
538 live_fetch(
539 "SLEY_REMOTE_LIVE_PRIVATE_HTTPS_URL",
540 "SLEY_REMOTE_LIVE_PRIVATE_HTTPS_BRANCH",
541 FetchSource::Http(remote),
542 &mut credentials,
543 None,
544 );
545 }
546
547 #[test]
548 fn live_https_push() {
549 let Some(url) = live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_URL") else {
550 return;
551 };
552 let remote = parse_remote_url(&url).expect("live HTTPS push URL should parse");
553 let mut no_credentials;
554 let mut env_credentials;
555 let credentials: &mut dyn CredentialProvider = match (
556 live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_USERNAME"),
557 live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_PASSWORD"),
558 ) {
559 (Some(username), Some(password)) => {
560 env_credentials = EnvCredentials { username, password };
561 &mut env_credentials
562 }
563 _ => {
564 no_credentials = NoCredentials;
565 &mut no_credentials
566 }
567 };
568 live_push(
569 "SLEY_REMOTE_LIVE_HTTPS_PUSH_URL",
570 "SLEY_REMOTE_LIVE_HTTPS_PUSH_BRANCH_PREFIX",
571 PushDestination::Http(remote),
572 credentials,
573 );
574 }
575
576 #[test]
577 fn live_ssh_fetch() {
578 let Some(url) = live_env("SLEY_REMOTE_LIVE_SSH_FETCH_URL") else {
579 return;
580 };
581 let remote = parse_remote_url(&url).expect("live SSH fetch URL should parse");
582 let mut credentials = NoCredentials;
583 live_fetch(
584 "SLEY_REMOTE_LIVE_SSH_FETCH_URL",
585 "SLEY_REMOTE_LIVE_SSH_FETCH_BRANCH",
586 FetchSource::Ssh(remote),
587 &mut credentials,
588 None,
589 );
590 }
591
592 #[test]
593 fn live_ssh_push() {
594 let Some(url) = live_env("SLEY_REMOTE_LIVE_SSH_PUSH_URL") else {
595 return;
596 };
597 let remote = parse_remote_url(&url).expect("live SSH push URL should parse");
598 let mut credentials = NoCredentials;
599 live_push(
600 "SLEY_REMOTE_LIVE_SSH_PUSH_URL",
601 "SLEY_REMOTE_LIVE_SSH_PUSH_BRANCH_PREFIX",
602 PushDestination::Ssh(remote),
603 &mut credentials,
604 );
605 }
606
607 #[test]
608 fn live_shallow_https_fetch_and_clone() {
609 let Some(url) = live_env("SLEY_REMOTE_LIVE_SHALLOW_HTTPS_URL") else {
610 return;
611 };
612 let branch =
613 live_env("SLEY_REMOTE_LIVE_SHALLOW_HTTPS_BRANCH").unwrap_or_else(|| "main".into());
614 let remote = parse_remote_url(&url).expect("live shallow HTTPS URL should parse");
615 let mut credentials = NoCredentials;
616 live_fetch(
617 "SLEY_REMOTE_LIVE_SHALLOW_HTTPS_URL",
618 "SLEY_REMOTE_LIVE_SHALLOW_HTTPS_BRANCH",
619 FetchSource::Http(remote.clone()),
620 &mut credentials,
621 Some(1),
622 );
623
624 let destination = std::env::temp_dir().join(format!(
625 "sley-remote-live-clone-{}-{}",
626 std::process::id(),
627 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
628 ));
629 let _ = fs::remove_dir_all(&destination);
630 let config = remote_config(&url);
631 let mut configure = |_git_dir: &Path| Ok(config.clone());
632 let mut configure_branch = |_git_dir: &Path, _branch: &str| Ok(config.clone());
633 let options = CloneOptions {
634 origin: "origin",
635 checkout_branch: &branch,
636 remote_head_branch: &branch,
637 single_branch: true,
638 depth: Some(1),
639 deepen_since: None,
640 deepen_not: Vec::new(),
641 committer: b"Sley Remote Live <sley@example.invalid> 1 +0000".to_vec(),
642 detached_head: None,
643 checkout: true,
644 filter: None,
645 branch_explicit: true,
648 ref_storage: sley_formats::RefStorageFormat::Files,
649 ssh_options: None,
650 reject_shallow: false,
651 };
652 let mut clone_credentials = NoCredentials;
653 let mut progress = SilentProgress;
654
655 let outcome = clone(
656 CloneRequest {
657 destination: &destination,
658 git_dir_override: None,
659 core_worktree: None,
660 format: ObjectFormat::Sha1,
661 source: &CloneSource::Http(RemoteUrl { ..remote }),
662 options: &options,
663 },
664 CloneServices {
665 configure: &mut configure,
666 configure_branch: &mut configure_branch,
667 credentials: &mut clone_credentials,
668 progress: &mut progress,
669 },
670 )
671 .expect("live shallow HTTPS clone should succeed");
672
673 assert!(outcome.git_dir.join("shallow").exists());
674 }
675}