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