Skip to main content

sley_remote/
lib.rs

1//! `git-remote` — callable fetch / push / clone / ls-remote orchestration.
2//!
3//! This crate lifts the network-transport orchestration out of the `git-cli`
4//! monolith so it can be driven as a library (the way a downstream consumer such
5//! as heddle needs). The wire codecs ([`sley_protocol`]), the pack encoder
6//! ([`sley_pack`]), pack building ([`sley_odb`]) and ref/commit plumbing already
7//! live in their own crates; `git-remote` is the glue that sequences them into
8//! `fetch`/`push`/`clone`/`ls-remote`, with the CLI-specific concerns (argument
9//! parsing, stdout/stderr formatting, exit codes, repository discovery from
10//! process-global state) kept out via the seams below:
11//!
12//! * [`CredentialProvider`] — how authenticated remotes obtain credentials. The
13//!   caller injects one (e.g. a credential-helper-backed impl, an interactive
14//!   prompt, or [`NoCredentials`] for unauthenticated/public access).
15//! * [`ProgressSink`] — where human-facing progress/summary lines go. The
16//!   orchestration returns structured outcomes and emits progress through this
17//!   sink instead of printing, so the caller controls presentation.
18//!
19//! The lift proceeds in stages (see `docs/git-remote-extraction.md`); this is
20//! the scaffold (stage A).
21
22use 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
171/// The object format of the repository whose common `$GIT_DIR` is `common_git_dir`.
172///
173/// Reads `common_git_dir/config`'s `extensions.objectFormat`, defaulting to
174/// SHA-1 when the config is absent or unreadable (matching git). `common_git_dir`
175/// must already be the common git dir; this does no worktree resolution.
176pub 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
183/// Supplies credentials for an authenticated remote, mirroring git's credential
184/// protocol: [`fill`](CredentialProvider::fill) is handed a partial
185/// [`GitCredential`] describing the request (protocol/host/path) and returns a
186/// completed credential, or `None` to proceed unauthenticated.
187///
188/// [`approve`](CredentialProvider::approve) / [`reject`](CredentialProvider::reject)
189/// let a backing store remember or forget a credential after the request
190/// succeeds or fails; the default no-ops suit providers without a store.
191pub trait CredentialProvider {
192    /// Complete `request` into a usable credential, or return `None` to attempt
193    /// the request without authentication.
194    fn fill(&mut self, request: GitCredential) -> Result<Option<GitCredential>>;
195
196    /// Record `credential` as having worked (e.g. store it). Default: no-op.
197    fn approve(&mut self, _credential: &GitCredential) -> Result<()> {
198        Ok(())
199    }
200
201    /// Record `credential` as having failed (e.g. erase it). Default: no-op.
202    fn reject(&mut self, _credential: &GitCredential) -> Result<()> {
203        Ok(())
204    }
205}
206
207/// A [`CredentialProvider`] that never supplies credentials, so every request is
208/// attempted unauthenticated. This is what an embedder targeting public remotes
209/// (e.g. heddle) uses to suppress prompts.
210#[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/// Structured transfer statistics during a fetch/clone pack receive+index.
220/// All monotonic within one operation; reset per operation.
221#[derive(Debug, Clone, Copy, Default)]
222pub struct TransferProgress {
223    /// Wire/pack bytes received so far. THE KB counter. No reliable total
224    /// (smart-HTTP doesn't announce pack size up front).
225    pub received_bytes: u64,
226    /// Objects received/parsed from the pack so far.
227    pub received_objects: u64,
228    /// Total objects the server announced (pack header), once known — the
229    /// denominator for an object-count percentage. `None` until the header
230    /// is read.
231    pub total_objects: Option<u64>,
232    /// Deltas resolved during the resolving-deltas phase (surface if cheap).
233    pub indexed_deltas: u64,
234}
235
236/// Receives human-facing progress and summary events from an operation (the
237/// "To <remote>" push summary, prune notices, "Cloning into…", etc.). The
238/// orchestration returns structured outcomes regardless; this is purely for
239/// presentation, so the default implementations discard everything.
240pub trait ProgressSink {
241    /// A free-form progress or summary line.
242    fn message(&mut self, _message: &str) {}
243    /// Structured transfer progress. Default: ignore (keeps existing impls valid).
244    fn transfer(&mut self, _progress: TransferProgress) {}
245}
246
247/// A [`ProgressSink`] that discards every event.
248#[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            // The live test clones a specific branch via --single-branch, so the
636            // branch was explicitly requested (a missing remote tip is a hard error).
637            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}