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