1use std::io::Read;
23use std::path::Path;
24
25use sley_config::GitConfig;
26pub use sley_config::remotes::{SetUrlError, SetUrlKind, SetUrlOp, set_url};
27use sley_core::{GitError, ObjectFormat, Result};
28use sley_protocol::{
29 RefAdvertisementSet, parse_ref_advertisement_set, parse_upload_pack_features,
30 read_pkt_line_frames_until_flush,
31};
32use sley_transport::GitCredential;
33
34mod admin;
35mod install;
36pub use admin::{
37 AddRemoteOptions, AddRemoteOutcome, RemoteAdminError, RemoteHeadMutation, RemoteHeadPlan,
38 RemoteMirror, RemotePrunePlan, RemoteTagMode, RemoveRemoteOutcome, RenameRemoteOutcome,
39 add_remote, apply_remote_head, plan_remote_prune, remote_branch_fetch_refspec, remove_remote,
40 rename_remote, set_remote_branches,
41};
42pub use install::{
43 install_protocol_v2_fetch_promisor_response_from_reader,
44 install_protocol_v2_fetch_response_from_reader,
45 install_upload_pack_packfile_promisor_response_from_reader,
46 install_upload_pack_packfile_response_from_reader,
47 install_upload_pack_raw_promisor_response_from_reader,
48 install_upload_pack_raw_response_from_reader,
49 install_upload_pack_shallow_packfile_promisor_response_from_reader,
50 install_upload_pack_shallow_packfile_response_from_reader,
51 install_upload_pack_shallow_raw_promisor_response_from_reader,
52 install_upload_pack_shallow_raw_response_from_reader,
53 shallow_info_from_protocol_v2_fetch_header,
54};
55
56mod credentials;
57pub use credentials::{
58 CredentialHelperProvider, credential_fill, credential_request_for_url, credential_store,
59 http_credential_host, http_protocol_name, http_url_credential,
60};
61
62mod http_backend;
63pub use http_backend::{
64 HttpBackendOperation, HttpBackendPlan, HttpBackendRequest, HttpBackendService,
65 http_backend_service_enabled, plan_http_backend_request,
66};
67
68#[cfg(feature = "http")]
69mod http;
70#[cfg(feature = "http")]
71pub use http::{
72 HttpFetchPackRequest, HttpOperationBatch, HttpServiceAdvertisements, HttpUploadPackDiscovery,
73 http_advertised_refs, http_authorization_headers, http_check_status, http_discover_upload_pack,
74 http_protocol_v2_fetch_response, http_send_with_auth, http_service_advertisements,
75 http_upload_pack_advertisements, http_upload_pack_features, http_validate_content_type,
76 install_fetch_pack_via_http_protocol_v2_fetch, install_fetch_pack_via_http_upload_pack,
77 new_http_client, remote_url_is_http,
78};
79#[cfg(feature = "http")]
84pub use sley_transport::{HttpClient, HttpResponse, UreqHttpClient};
85
86mod ssh;
87pub use ssh::{
88 SshFetchPackRequest, SshTransportOptions, SshUploadPackDiscovery,
89 discover_ssh_upload_pack_advertisements_with_command, install_fetch_pack_via_ssh_upload_pack,
90 ssh_program, ssh_transport_options_from_config, ssh_upload_pack_advertisements,
91 ssh_upload_pack_advertisements_with_command, ssh_upload_pack_advertisements_with_options,
92};
93
94mod git;
95mod git_proxy;
96pub use git::{
97 GitFetchPackRequest, discover_git_upload_pack_advertisements, git_upload_pack_advertisements,
98 git_upload_pack_advertisements_with_protocol, install_fetch_pack_via_git_upload_pack,
99};
100
101mod helper;
102pub use helper::{
103 DiscoveredRemoteHelperFetchOperation, RemoteHelperCapabilities, RemoteHelperEvent,
104 RemoteHelperEventSink, RemoteHelperExportRequest, RemoteHelperFetchDiscovery,
105 RemoteHelperFetchOperation, RemoteHelperFetchServices, RemoteHelperPlumbing,
106 RemoteHelperPushError, RemoteHelperPushOperation, RemoteHelperPushOptions,
107 RemoteHelperPushOutcome, RemoteHelperPushServices, RemoteHelperRef, RemoteHelperRefValue,
108 RemoteHelperSession, RemoteHelperSpec, discover_remote_helper_fetch,
109 fetch_via_discovered_remote_helper, fetch_via_remote_helper,
110 imported_remote_helper_advertisements, push_via_remote_helper, resolve_remote_helper,
111 rewrite_remote_helper_import_stream,
112};
113
114mod proc_receive;
115mod receive_hooks;
116pub use receive_hooks::{run_pre_receive, run_update_hooks};
117mod receive_pack_server;
118
119pub use proc_receive::{
120 ProcReceiveRefPattern, ProcReceiveReport, ReceivePackCommandState, parse_proc_receive_refs,
121 proc_receive_ref_matches,
122};
123pub use receive_pack_server::{
124 ReceivePackServerOptions, ReceivePackServerOutcome, ReceivePackServerReport,
125 ReceivePackServerRequest, flush_receive_pack_sideband, receive_pack_server_report_v1,
126 request_uses_sideband, run_receive_pack_post_hooks, serve_receive_pack,
127 write_receive_pack_server_report, write_receive_pack_sideband_stderr,
128};
129
130pub(crate) fn read_discovered_upload_pack_advertisements(
136 reader: &mut impl Read,
137) -> Result<(RefAdvertisementSet, ObjectFormat)> {
138 let frames = read_pkt_line_frames_until_flush(reader)?;
139 let sha1 = parse_ref_advertisement_set(ObjectFormat::Sha1, &frames);
140 let sha256 = parse_ref_advertisement_set(ObjectFormat::Sha256, &frames);
141 for (format, parsed) in [
142 (ObjectFormat::Sha1, sha1.as_ref()),
143 (ObjectFormat::Sha256, sha256.as_ref()),
144 ] {
145 let Ok(set) = parsed else {
146 continue;
147 };
148 let advertised = set
149 .refs
150 .first()
151 .map(|reference| parse_upload_pack_features(&reference.capabilities))
152 .transpose()?
153 .and_then(|features| features.object_format)
154 .unwrap_or(ObjectFormat::Sha1);
155 if advertised == format {
156 return Ok((set.clone(), format));
157 }
158 }
159 match sha1 {
160 Err(err) => Err(err),
161 Ok(_) => Err(GitError::InvalidObjectId(
162 "advertised object format does not match advertised object ids".into(),
163 )),
164 }
165}
166
167mod local;
168pub use local::{
169 INFINITE_DEPTH, LocalDeepenPlan, attach_receive_pack_capabilities,
170 attach_upload_pack_capabilities, compute_local_deepen, compute_local_deepen_by_rev_list,
171 hydrate_objects_from_local_promisor_remotes, hydrate_reachable_from_local_promisor_remotes,
172 install_fetch_pack_via_local_upload_pack, local_fetch_advertisements, local_have_oids,
173 receive_pack_features, receive_pack_into_local_repository,
174 receive_pack_request_uses_push_options, receive_pack_stream_into_local_repository,
175 serve_upload_pack_v2, serve_upload_pack_v2_stateless_with_config,
176 serve_upload_pack_v2_with_config, upload_pack_features, upload_pack_from_local_repository,
177 upload_pack_request_uses_sideband, upload_pack_sideband_response,
178};
179
180mod filter;
181pub use filter::{pack_filter_from_spec, pack_filter_from_spec_for_clone};
182
183mod fetch;
184#[cfg(feature = "http")]
185pub use fetch::fetch_with_http_client;
186pub use fetch::{
187 FetchOptions, FetchOutcome, FetchRepositoryPlan, FetchRequest, FetchServices, FetchSource,
188 PruneRefsInput, PrunedRef, RemoteHelperFetchRequest, append_reachable_auto_follow_tags,
189 apply_configured_fetch_prune_option, apply_configured_partial_clone_filter,
190 apply_configured_remote_tag_option, fetch, fetch_head_source_description,
191 fetch_refspec_excludes, fetch_refspecs_for_source, finalize_remote_helper_fetch,
192 mark_tag_refspec_updates_not_for_merge, order_bundle_fetch_all_tags_updates,
193 plan_fetch_repository, prune_refs_from_advertisements, retain_missing_auto_follow_tags,
194 write_default_fetch_head, write_fetch_head, write_fetch_head_records,
195};
196
197mod pack;
198pub use pack::{
199 PushPackRequest, build_push_packfile, build_receive_pack_body,
200 remote_advertisement_tips_known_to_local,
201};
202
203mod push;
204pub use push::{
205 PushAction, PushActionPlan, PushActionRequest, PushCommand, PushDestination, PushOptions,
206 PushOutcome, PushPlan, PushQuarantine, PushRefStatus, PushReportRef, PushReportRequest,
207 PushRequest, PushServices, PushStatusReport, PushThinMode, ReceivePackPushReport,
208 apply_receive_pack_report_to_push_refs, execute_push_action_plan, execute_push_plan,
209 local_push_source_refs, normalize_push_refname, normalize_push_refspec, plan_push,
210 plan_push_actions, push, push_actions, push_local_uses_receive_pack_server,
211 push_local_with_report, push_local_with_report_and_objects, push_url_for_display,
212 read_receive_pack_push_report, reject_non_fast_forward_pushes, run_local_push_post_hooks,
213 stage_local_push_quarantine, validate_receive_pack_report, validate_receive_pack_unpack,
214};
215
216mod ls_remote;
217pub use ls_remote::{
218 LsRemoteFilter, LsRemoteOutcome, LsRemoteRecord, LsRemoteRequest, LsRemoteSource, ls_remote,
219 ls_remote_with,
220};
221
222mod clone;
223#[cfg(feature = "http")]
224pub use clone::clone_with_http_client;
225pub use clone::{CloneOptions, CloneOutcome, CloneRequest, CloneServices, CloneSource, clone};
226
227mod bundle;
228pub use bundle::{FetchBundleRequest, fetch_bundle};
229mod bundle_uri;
230pub use bundle_uri::{
231 BundleUriEntry, BundleUriList, bundle_uri_fetch_order, handshake_advertises_bundle_uri,
232 http_remote_bundle_uri_list, parse_bundle_uri_line, prefetch_advertised_bundle_uris,
233 prefetch_advertised_bundle_uris_with_client, transfer_bundle_uri_enabled,
234};
235
236mod shallow;
237pub use shallow::{apply_shallow_info, read_shallow, write_shallow};
238
239mod capabilities;
240pub use capabilities::{RemoteTransportKind, TransportCapabilities};
241
242mod protocol;
243pub use protocol::{
244 TransportPolicyError, check_transport_allowed, is_transport_allowed,
245 transport_scheme_for_remote, transport_scheme_for_url,
246};
247
248mod promisor;
249pub use promisor::{
250 PromisorAcceptPolicy, PromisorRemoteDecision, PromisorRemoteField, PromisorRemoteFieldUpdate,
251 apply_promisor_remote_field_updates, config_has_promisor_remote,
252 configured_promisor_remote_names, decide_promisor_remote_reply, promisor_accept_policy,
253 promisor_remote_auto_filter, promisor_remote_server_capability,
254};
255
256mod resolve;
257pub use resolve::{
258 RemoteResolutionContext, ResolvedRemote, discover_local_git_dir, fetch_source_for_url,
259 fetch_url, push_destination_for_url, push_url, resolve_configured_local_remote_git_dir,
260 resolve_fetch_source, resolve_local_remote_git_dir, resolve_push_destination, resolve_remote,
261 transport_kind_for_url,
262};
263
264pub fn object_format_for_git_dir(common_git_dir: &Path) -> Result<ObjectFormat> {
270 let Ok(config) = GitConfig::read(common_git_dir.join("config")) else {
271 return Ok(ObjectFormat::Sha1);
272 };
273 config.repository_object_format()
274}
275
276pub trait CredentialProvider {
285 fn fill(&mut self, request: GitCredential) -> Result<Option<GitCredential>>;
288
289 fn approve(&mut self, _credential: &GitCredential) -> Result<()> {
291 Ok(())
292 }
293
294 fn reject(&mut self, _credential: &GitCredential) -> Result<()> {
296 Ok(())
297 }
298}
299
300#[derive(Debug, Default, Clone, Copy)]
304pub struct NoCredentials;
305
306impl CredentialProvider for NoCredentials {
307 fn fill(&mut self, _request: GitCredential) -> Result<Option<GitCredential>> {
308 Ok(None)
309 }
310}
311
312#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
315pub struct TransferProgress {
316 pub received_bytes: u64,
319 pub received_objects: u64,
321 pub total_objects: Option<u64>,
325 pub indexed_deltas: u64,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub struct PackGenerationProgress {
335 pub total_objects: usize,
337 pub compression_objects: usize,
339 pub delta_objects: u32,
341}
342
343pub trait ProgressSink {
348 fn transfer(&mut self, _progress: TransferProgress) {}
350
351 fn pack_generation(&mut self, _progress: &PackGenerationProgress) {}
353
354 fn message(&mut self, _message: &str) {}
356 fn diagnostic(&mut self, message: &str) {
358 self.message(message);
359 }
360}
361
362#[derive(Debug, Default, Clone, Copy)]
364pub struct SilentProgress;
365
366impl ProgressSink for SilentProgress {}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use std::fs;
372 use std::path::{Path, PathBuf};
373 use std::sync::atomic::{AtomicU64, Ordering};
374
375 use sley_config::{ConfigEntry, ConfigSection};
376 use sley_formats::RepositoryLayout;
377 use sley_object::{Commit, EncodedObject, ObjectType, Tree};
378 use sley_odb::{FileObjectDatabase, ObjectWriter};
379 use sley_refs::{FileRefStore, RefTarget, RefUpdate};
380 use sley_transport::{RemoteUrl, parse_remote_url};
381
382 #[test]
383 fn clone_discovery_adopts_advertised_sha256_format() {
384 let advertised = sley_protocol::RefAdvertisementSet {
385 protocol: sley_protocol::ProtocolVersion::V0,
386 refs: vec![sley_protocol::RefAdvertisement {
387 oid: sley_core::ObjectId::null(ObjectFormat::Sha256),
388 name: "capabilities^{}".into(),
389 capabilities: vec![sley_core::Capability {
390 name: "object-format".into(),
391 value: Some("sha256".into()),
392 }],
393 }],
394 shallow: Vec::new(),
395 };
396 let mut bytes = Vec::new();
397 sley_protocol::write_ref_advertisement_set(&mut bytes, &advertised)
398 .expect("advertisement should encode");
399
400 let (parsed, format) = read_discovered_upload_pack_advertisements(&mut bytes.as_slice())
401 .expect("format should be discovered");
402
403 assert_eq!(format, ObjectFormat::Sha256);
404 assert_eq!(parsed, advertised);
405 }
406
407 #[test]
408 fn no_credentials_never_fills() {
409 let mut provider = NoCredentials;
410 let request = GitCredential::default();
411 assert!(
412 provider
413 .fill(request)
414 .expect("test operation should succeed")
415 .is_none()
416 );
417 }
418
419 #[test]
420 fn silent_progress_accepts_messages() {
421 let mut progress = SilentProgress;
422 progress.message("Cloning into 'x'...");
423 }
424
425 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
426
427 fn live_env(name: &str) -> Option<String> {
428 match std::env::var(name) {
429 Ok(value) if !value.is_empty() => Some(value),
430 _ => None,
431 }
432 }
433
434 fn live_repo(name: &str) -> PathBuf {
435 let dir = std::env::temp_dir().join(format!(
436 "sley-remote-live-{name}-{}-{}",
437 std::process::id(),
438 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
439 ));
440 let _ = fs::remove_dir_all(&dir);
441 RepositoryLayout::init_at(&dir, ObjectFormat::Sha1, false)
442 .expect("live test repository should initialize");
443 dir.join(".git")
444 }
445
446 fn remote_config(url: &str) -> GitConfig {
447 GitConfig {
448 sections: vec![ConfigSection::new(
449 "remote",
450 Some("origin".into()),
451 vec![
452 ConfigEntry::new("url", Some(url.into())),
453 ConfigEntry::new("fetch", Some("+refs/heads/*:refs/remotes/origin/*".into())),
454 ],
455 )],
456 ..GitConfig::default()
457 }
458 }
459
460 fn fetch_options(depth: Option<u32>) -> FetchOptions {
461 FetchOptions {
462 quiet: true,
463 progress: None,
464 auto_follow_tags: false,
465 fetch_all_tags: false,
466 prune: false,
467 prune_tags: false,
468 dry_run: false,
469 force: false,
470 append: false,
471 write_fetch_head: true,
472 tag_option_explicit: true,
473 prune_option_explicit: true,
474 prune_tags_option_explicit: true,
475 refmap: None,
476 depth,
477 merge_srcs: Vec::new(),
478 filter: None,
479 filter_auto: false,
480 refetch: false,
481 cloning: false,
482 record_promisor_refs: true,
483 update_shallow: false,
484 reject_shallow: false,
485 deepen_relative: false,
486 update_head_ok: false,
487 deepen_since: None,
488 deepen_not: Vec::new(),
489 ssh_options: None,
490 upload_pack_command: None,
491 atomic: false,
492 negotiation_restrict: None,
493 negotiation_include: None,
494 }
495 }
496
497 fn write_live_commit(git_dir: &Path, branch: &str) {
498 let format = ObjectFormat::Sha1;
499 let db = FileObjectDatabase::from_git_dir(git_dir, format);
500 let tree = db
501 .write_object(EncodedObject::new(
502 ObjectType::Tree,
503 Tree { entries: vec![] }.write(),
504 ))
505 .expect("live commit tree should write");
506 let timestamp = 1 + TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
507 let identity =
508 format!("Sley Remote Live <sley@example.invalid> {timestamp} +0000").into_bytes();
509 let oid = db
510 .write_object(EncodedObject::new(
511 ObjectType::Commit,
512 Commit {
513 tree,
514 parents: Vec::new(),
515 author: identity.clone(),
516 committer: identity,
517 encoding: None,
518 message: format!("sley remote live {branch}\n").into_bytes(),
519 }
520 .write(),
521 ))
522 .expect("live commit should write");
523 let store = FileRefStore::new(git_dir, format);
524 let mut tx = store.transaction();
525 tx.update(RefUpdate {
526 name: format!("refs/heads/{branch}"),
527 expected: None,
528 new: RefTarget::Direct(oid),
529 reflog: None,
530 });
531 tx.update(RefUpdate {
532 name: "HEAD".into(),
533 expected: None,
534 new: RefTarget::Symbolic(format!("refs/heads/{branch}")),
535 reflog: None,
536 });
537 tx.commit().expect("live refs should update");
538 }
539
540 struct EnvCredentials {
541 username: String,
542 password: String,
543 }
544
545 impl CredentialProvider for EnvCredentials {
546 fn fill(&mut self, mut request: GitCredential) -> Result<Option<GitCredential>> {
547 request.username = Some(self.username.clone());
548 request.password = Some(self.password.clone());
549 Ok(Some(request))
550 }
551 }
552
553 fn live_fetch(
554 url_var: &str,
555 branch_var: &str,
556 source: FetchSource,
557 credentials: &mut dyn CredentialProvider,
558 depth: Option<u32>,
559 ) {
560 let Some(url) = live_env(url_var) else {
561 return;
562 };
563 let branch = live_env(branch_var).unwrap_or_else(|| "main".into());
564 let local = live_repo(url_var);
565 let refspec = format!("refs/heads/{branch}:refs/remotes/origin/{branch}");
566 let config = remote_config(&url);
567 let options = fetch_options(depth);
568 let mut progress = SilentProgress;
569
570 let outcome = fetch(
571 FetchRequest {
572 git_dir: &local,
573 format: ObjectFormat::Sha1,
574 config: &config,
575 remote_name: "origin",
576 source: &source,
577 refspecs: &[refspec],
578 options: &options,
579 validation: None,
580 },
581 FetchServices {
582 credentials,
583 progress: &mut progress,
584 ref_hook: None,
585 },
586 )
587 .expect("live fetch should succeed");
588
589 assert!(!outcome.ref_updates.is_empty());
590 if depth.is_some() {
591 assert!(
592 local.join("shallow").exists(),
593 "shallow fetch should write .git/shallow"
594 );
595 }
596 }
597
598 fn live_push(
599 url_var: &str,
600 branch_prefix_var: &str,
601 destination: PushDestination,
602 credentials: &mut dyn CredentialProvider,
603 ) {
604 let Some(_) = live_env(url_var) else {
605 return;
606 };
607 let branch_prefix =
608 live_env(branch_prefix_var).unwrap_or_else(|| "sley-remote-live".into());
609 let branch = format!(
610 "{branch_prefix}-{}-{}",
611 std::process::id(),
612 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
613 );
614 let local = live_repo(url_var);
615 write_live_commit(&local, &branch);
616 let refspec = format!("refs/heads/{branch}:refs/heads/{branch}");
617 let options = PushOptions {
618 quiet: true,
619 force: false,
620 thin: PushThinMode::Auto,
621 atomic: false,
622 push_options: Vec::new(),
623 };
624 let mut progress = SilentProgress;
625
626 let outcome = push(
627 PushRequest {
628 git_dir: &local,
629 common_git_dir: &local,
630 format: ObjectFormat::Sha1,
631 config: &GitConfig::default(),
632 remote: "origin",
633 destination: &destination,
634 refspecs: &[refspec],
635 options: &options,
636 },
637 PushServices {
638 credentials,
639 progress: &mut progress,
640 },
641 )
642 .expect("live push should succeed");
643
644 assert_eq!(outcome.commands.len(), 1);
645 }
646
647 #[test]
648 fn live_github_https_public_fetch() {
649 let Some(url) = live_env("SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_URL") else {
650 return;
651 };
652 let remote = parse_remote_url(&url).expect("live HTTPS URL should parse");
653 let mut credentials = NoCredentials;
654 live_fetch(
655 "SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_URL",
656 "SLEY_REMOTE_LIVE_GITHUB_HTTPS_PUBLIC_BRANCH",
657 FetchSource::Http(remote),
658 &mut credentials,
659 None,
660 );
661 }
662
663 #[test]
664 fn live_private_https_auth_fetch_uses_credential_provider() {
665 let (Some(url), Some(username), Some(password)) = (
666 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_URL"),
667 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_USERNAME"),
668 live_env("SLEY_REMOTE_LIVE_PRIVATE_HTTPS_PASSWORD"),
669 ) else {
670 return;
671 };
672 let remote = parse_remote_url(&url).expect("live private HTTPS URL should parse");
673 let mut credentials = EnvCredentials { username, password };
674 live_fetch(
675 "SLEY_REMOTE_LIVE_PRIVATE_HTTPS_URL",
676 "SLEY_REMOTE_LIVE_PRIVATE_HTTPS_BRANCH",
677 FetchSource::Http(remote),
678 &mut credentials,
679 None,
680 );
681 }
682
683 #[test]
684 fn live_https_push() {
685 let Some(url) = live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_URL") else {
686 return;
687 };
688 let remote = parse_remote_url(&url).expect("live HTTPS push URL should parse");
689 let mut no_credentials;
690 let mut env_credentials;
691 let credentials: &mut dyn CredentialProvider = match (
692 live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_USERNAME"),
693 live_env("SLEY_REMOTE_LIVE_HTTPS_PUSH_PASSWORD"),
694 ) {
695 (Some(username), Some(password)) => {
696 env_credentials = EnvCredentials { username, password };
697 &mut env_credentials
698 }
699 _ => {
700 no_credentials = NoCredentials;
701 &mut no_credentials
702 }
703 };
704 live_push(
705 "SLEY_REMOTE_LIVE_HTTPS_PUSH_URL",
706 "SLEY_REMOTE_LIVE_HTTPS_PUSH_BRANCH_PREFIX",
707 PushDestination::Http(remote),
708 credentials,
709 );
710 }
711
712 #[test]
713 fn live_ssh_fetch() {
714 let Some(url) = live_env("SLEY_REMOTE_LIVE_SSH_FETCH_URL") else {
715 return;
716 };
717 let remote = parse_remote_url(&url).expect("live SSH fetch URL should parse");
718 let mut credentials = NoCredentials;
719 live_fetch(
720 "SLEY_REMOTE_LIVE_SSH_FETCH_URL",
721 "SLEY_REMOTE_LIVE_SSH_FETCH_BRANCH",
722 FetchSource::Ssh(remote),
723 &mut credentials,
724 None,
725 );
726 }
727
728 #[test]
729 fn live_ssh_push() {
730 let Some(url) = live_env("SLEY_REMOTE_LIVE_SSH_PUSH_URL") else {
731 return;
732 };
733 let remote = parse_remote_url(&url).expect("live SSH push URL should parse");
734 let mut credentials = NoCredentials;
735 live_push(
736 "SLEY_REMOTE_LIVE_SSH_PUSH_URL",
737 "SLEY_REMOTE_LIVE_SSH_PUSH_BRANCH_PREFIX",
738 PushDestination::Ssh(remote),
739 &mut credentials,
740 );
741 }
742
743 #[test]
744 fn live_shallow_https_fetch_and_clone() {
745 let Some(url) = live_env("SLEY_REMOTE_LIVE_SHALLOW_HTTPS_URL") else {
746 return;
747 };
748 let branch =
749 live_env("SLEY_REMOTE_LIVE_SHALLOW_HTTPS_BRANCH").unwrap_or_else(|| "main".into());
750 let remote = parse_remote_url(&url).expect("live shallow HTTPS URL should parse");
751 let mut credentials = NoCredentials;
752 live_fetch(
753 "SLEY_REMOTE_LIVE_SHALLOW_HTTPS_URL",
754 "SLEY_REMOTE_LIVE_SHALLOW_HTTPS_BRANCH",
755 FetchSource::Http(remote.clone()),
756 &mut credentials,
757 Some(1),
758 );
759
760 let destination = std::env::temp_dir().join(format!(
761 "sley-remote-live-clone-{}-{}",
762 std::process::id(),
763 TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
764 ));
765 let _ = fs::remove_dir_all(&destination);
766 let config = remote_config(&url);
767 let mut configure = |_git_dir: &Path| Ok(config.clone());
768 let mut configure_branch = |_git_dir: &Path, _branch: &str| Ok(config.clone());
769 let options = CloneOptions {
770 origin: "origin",
771 checkout_branch: &branch,
772 remote_head_branch: &branch,
773 single_branch: true,
774 progress: false,
775 depth: Some(1),
776 deepen_since: None,
777 deepen_not: Vec::new(),
778 committer: b"Sley Remote Live <sley@example.invalid> 1 +0000".to_vec(),
779 detached_head: None,
780 checkout: true,
781 filter: None,
782 filter_auto: false,
783 branch_explicit: true,
786 ref_storage: sley_formats::RefStorageFormat::Files,
787 ssh_options: None,
788 upload_pack_command: None,
789 reject_shallow: false,
790 };
791 let mut clone_credentials = NoCredentials;
792 let mut progress = SilentProgress;
793
794 let outcome = clone(
795 CloneRequest {
796 destination: &destination,
797 git_dir_override: None,
798 core_worktree: None,
799 format: ObjectFormat::Sha1,
800 source: &CloneSource::Http(RemoteUrl { ..remote }),
801 options: &options,
802 },
803 CloneServices {
804 configure: &mut configure,
805 configure_branch: &mut configure_branch,
806 credentials: &mut clone_credentials,
807 progress: &mut progress,
808 },
809 )
810 .expect("live shallow HTTPS clone should succeed");
811
812 assert!(outcome.git_dir.join("shallow").exists());
813 }
814}