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