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