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