Skip to main content

sley_remote/
git.rs

1//! Native `git://` transport over a raw TCP stream.
2//!
3//! This is the anonymous git-daemon protocol: send one service request pkt-line,
4//! then speak upload-pack / receive-pack over the selected wire protocol.
5
6use std::collections::HashMap;
7use std::io::{Read, Write};
8use std::net::Shutdown;
9use std::path::Path;
10
11use sley_config::GitConfig;
12
13use crate::install::{
14    ProgressInstaller, install_protocol_v2_fetch_promisor_response_from_reader,
15    install_protocol_v2_fetch_response_from_reader,
16    install_upload_pack_raw_promisor_response_from_reader,
17    install_upload_pack_raw_response_from_reader,
18    install_upload_pack_shallow_raw_promisor_response_from_reader,
19    install_upload_pack_shallow_raw_response_from_reader,
20    shallow_info_from_protocol_v2_fetch_header,
21};
22use sley_core::{Capability, GitError, ObjectFormat, ObjectId, Result};
23use sley_odb::FileObjectDatabase;
24use sley_protocol::{
25    GitService, ProtocolV2CommandOptions, ProtocolV2FetchRequest, ProtocolV2FetchShallowInfo,
26    ProtocolV2LsRefsRequest, ProtocolVersion, ReceivePackCommand, ReceivePackFeatures,
27    RefAdvertisement, TransportHandshake, UploadPackFeatures, UploadPackNegotiationRequest,
28    UploadPackRequest, parse_protocol_v2_fetch_features, parse_receive_pack_features,
29    parse_refspec, parse_upload_pack_features, plan_push_commands,
30    protocol_v2_ls_refs_records_to_ref_advertisement_set, protocol_v2_object_format,
31    read_protocol_v2_advertisement, read_protocol_v2_ls_refs_response, read_ref_advertisement_set,
32    write_protocol_v2_command_request, write_protocol_v2_fetch_request,
33    write_upload_pack_negotiation_request, write_upload_pack_request,
34};
35use sley_refs::FileRefStore;
36use sley_transport::{RemoteTransport, RemoteUrl, ServiceRequest, write_service_request};
37
38use crate::git_proxy::GitConnection;
39use crate::{ProgressSink, PushOutcome, PushRequest};
40
41const GIT_DAEMON_PORT: u16 = 9418;
42
43pub(crate) struct GitPushRequest<'a> {
44    pub git_dir: &'a Path,
45    pub common_git_dir: &'a Path,
46    pub format: ObjectFormat,
47    pub remote: &'a RemoteUrl,
48    pub config: Option<&'a GitConfig>,
49    pub refspecs: &'a [String],
50    pub force: bool,
51}
52
53pub(crate) struct GitPushCommandsRequest<'a> {
54    pub common_git_dir: &'a Path,
55    pub format: ObjectFormat,
56    pub remote: &'a RemoteUrl,
57    pub config: Option<&'a GitConfig>,
58    pub command_forces: Vec<(ReceivePackCommand, bool)>,
59    pub pack_objects: Vec<ObjectId>,
60}
61
62pub(crate) struct GitPushPlan {
63    pub(crate) commands: Vec<ReceivePackCommand>,
64    pub(crate) pack_objects: Vec<ObjectId>,
65    stream: Option<GitConnection>,
66    features: ReceivePackFeatures,
67    advertisements: Vec<RefAdvertisement>,
68}
69
70pub struct GitFetchPackRequest<'a> {
71    pub git_dir: &'a Path,
72    pub format: ObjectFormat,
73    pub remote: &'a RemoteUrl,
74    pub config: Option<&'a GitConfig>,
75    pub features: &'a UploadPackFeatures,
76    pub wants: Vec<ObjectId>,
77    pub haves: Option<Vec<ObjectId>>,
78    pub shallow: Vec<ObjectId>,
79    pub deepen: Option<u32>,
80    pub promisor: bool,
81    pub protocol_v2: bool,
82    /// Maximum raw pack bytes to accept from the remote (`fetch.maxInputSize` /
83    /// `transfer.maxSize`). `None` means unlimited.
84    pub max_input_size: Option<u64>,
85}
86
87pub struct GitUploadPackAdvertisements {
88    pub refs: Vec<RefAdvertisement>,
89    pub features: UploadPackFeatures,
90    pub protocol_v2: bool,
91    pub object_format: ObjectFormat,
92}
93
94pub(crate) fn ls_remote_git(
95    remote: &RemoteUrl,
96    filter: &crate::ls_remote::LsRemoteFilter,
97    matches: &dyn Fn(&str) -> bool,
98    protocol_v2: bool,
99    config: Option<&GitConfig>,
100) -> Result<(Vec<crate::ls_remote::LsRemoteRecord>, ObjectFormat)> {
101    let set = if protocol_v2 {
102        git_protocol_v2_ls_refs(remote, ObjectFormat::Sha1, config)?
103    } else {
104        let mut stream = connect_git_service(remote, GitService::UploadPack, None, config)?;
105        read_ref_advertisement_set(ObjectFormat::Sha1, &mut stream)?
106    };
107    let features = set
108        .refs
109        .first()
110        .map(|advertisement| parse_upload_pack_features(&advertisement.capabilities))
111        .transpose()?
112        .unwrap_or_default();
113    let format = features.object_format.unwrap_or(ObjectFormat::Sha1);
114    if format != ObjectFormat::Sha1 {
115        return Err(GitError::Unsupported(format!(
116            "git:// ls-remote currently supports SHA-1 advertisements, got {}",
117            format.name()
118        )));
119    }
120    let symrefs = features
121        .symrefs
122        .iter()
123        .filter_map(|symref| symref.split_once(':'))
124        .map(|(name, target)| (name.to_string(), target.to_string()))
125        .collect::<HashMap<_, _>>();
126    let mut records = Vec::new();
127    for advertisement in set.refs {
128        if advertisement.oid.is_null() {
129            continue;
130        }
131        if filter.refs_only && (advertisement.name == "HEAD" || advertisement.name.ends_with("^{}"))
132        {
133            continue;
134        }
135        let is_head = advertisement.name.starts_with("refs/heads/");
136        let is_tag = advertisement.name.starts_with("refs/tags/");
137        if (filter.heads || filter.tags) && !((filter.heads && is_head) || (filter.tags && is_tag))
138        {
139            continue;
140        }
141        if !matches(&advertisement.name) {
142            continue;
143        }
144        records.push(crate::ls_remote::LsRemoteRecord {
145            oid: advertisement.oid,
146            symref: symrefs.get(&advertisement.name).cloned(),
147            name: advertisement.name,
148        });
149    }
150    Ok((records, format))
151}
152
153pub fn git_upload_pack_advertisements(
154    remote: &RemoteUrl,
155    format: ObjectFormat,
156) -> Result<GitUploadPackAdvertisements> {
157    git_upload_pack_advertisements_with_protocol(remote, format, false, None)
158}
159
160pub fn git_upload_pack_advertisements_with_protocol(
161    remote: &RemoteUrl,
162    format: ObjectFormat,
163    protocol_v2: bool,
164    config: Option<&GitConfig>,
165) -> Result<GitUploadPackAdvertisements> {
166    if protocol_v2 {
167        let mut stream = connect_git_service(
168            remote,
169            GitService::UploadPack,
170            Some(ProtocolVersion::V2),
171            config,
172        )?;
173        let handshake = read_protocol_v2_advertisement(&mut stream)?;
174        let object_format = protocol_v2_object_format(&handshake.capabilities)?;
175        if object_format != format {
176            return Err(GitError::InvalidObjectId(format!(
177                "remote repository uses {}, local repository uses {}",
178                object_format.name(),
179                format.name()
180            )));
181        }
182        let set = git_protocol_v2_ls_refs_on_stream(format, &mut stream)?;
183        let features = upload_pack_features_from_v2(&handshake, &set.refs)?;
184        return Ok(GitUploadPackAdvertisements {
185            refs: set.refs,
186            features,
187            protocol_v2: true,
188            object_format: format,
189        });
190    }
191
192    let mut stream = connect_git_service(remote, GitService::UploadPack, None, config)?;
193    let set = read_ref_advertisement_set(format, &mut stream)?;
194    let features = set
195        .refs
196        .first()
197        .map(|advertisement| parse_upload_pack_features(&advertisement.capabilities))
198        .transpose()?
199        .unwrap_or_default();
200    Ok(GitUploadPackAdvertisements {
201        refs: set.refs,
202        features,
203        protocol_v2: false,
204        object_format: format,
205    })
206}
207
208/// Discover upload-pack capabilities and object format before creating a clone.
209pub fn discover_git_upload_pack_advertisements(
210    remote: &RemoteUrl,
211    protocol_v2: bool,
212    config: Option<&GitConfig>,
213) -> Result<GitUploadPackAdvertisements> {
214    if protocol_v2 {
215        let mut stream = connect_git_service(
216            remote,
217            GitService::UploadPack,
218            Some(ProtocolVersion::V2),
219            config,
220        )?;
221        let handshake = read_protocol_v2_advertisement(&mut stream)?;
222        let object_format = protocol_v2_object_format(&handshake.capabilities)?;
223        let set = git_protocol_v2_ls_refs_on_stream(object_format, &mut stream)?;
224        let features = upload_pack_features_from_v2(&handshake, &set.refs)?;
225        return Ok(GitUploadPackAdvertisements {
226            refs: set.refs,
227            features,
228            protocol_v2: true,
229            object_format,
230        });
231    }
232
233    let mut stream = connect_git_service(remote, GitService::UploadPack, None, config)?;
234    let (set, object_format) = crate::read_discovered_upload_pack_advertisements(&mut stream)?;
235    let features = set
236        .refs
237        .first()
238        .map(|advertisement| parse_upload_pack_features(&advertisement.capabilities))
239        .transpose()?
240        .unwrap_or_default();
241    Ok(GitUploadPackAdvertisements {
242        refs: set.refs,
243        features,
244        protocol_v2: false,
245        object_format,
246    })
247}
248
249pub fn install_fetch_pack_via_git_upload_pack(
250    request: GitFetchPackRequest<'_>,
251    progress: &mut dyn ProgressSink,
252) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
253    if request.wants.is_empty() {
254        return Ok(Vec::new());
255    }
256    let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
257    if request.deepen.is_none() && all_wants_present(&local_db, &request.wants)? {
258        return Ok(Vec::new());
259    }
260    let haves = request
261        .haves
262        .clone()
263        .map(Ok)
264        .unwrap_or_else(|| crate::local::local_have_oids(request.git_dir, request.format))?;
265    if request.protocol_v2 {
266        return git_protocol_v2_fetch_into_repository(&request, haves, &local_db, progress);
267    }
268
269    let upload_request = UploadPackRequest {
270        wants: request.wants,
271        capabilities: shallow_request_capabilities(request.deepen),
272        shallow: request.shallow,
273        deepen: request.deepen,
274        ..UploadPackRequest::default()
275    };
276    let mut stream =
277        connect_git_service(request.remote, GitService::UploadPack, None, request.config)?;
278    read_ref_advertisement_set(request.format, &mut stream)?;
279    write_upload_pack_request(&mut stream, Some(&upload_request))?;
280    write_upload_pack_negotiation_request(
281        &mut stream,
282        &UploadPackNegotiationRequest { haves, done: true },
283    )?;
284    stream.flush()?;
285    let _ = stream.shutdown(Shutdown::Write);
286    if request.deepen.is_some() {
287        let shallow_info = if request.promisor {
288            let (shallow_info, _) = install_upload_pack_shallow_raw_promisor_response_from_reader(
289                request.format,
290                &mut stream,
291                &local_db,
292                request.max_input_size,
293            )?;
294            shallow_info
295        } else {
296            let (shallow_info, _) = install_upload_pack_shallow_raw_response_from_reader(
297                request.format,
298                &mut stream,
299                &ProgressInstaller::new(&local_db, progress),
300                request.max_input_size,
301            )?;
302            shallow_info
303        };
304        return Ok(shallow_info);
305    }
306    if request.promisor {
307        install_upload_pack_raw_promisor_response_from_reader(
308            request.format,
309            &mut stream,
310            &local_db,
311            request.max_input_size,
312        )?;
313    } else {
314        install_upload_pack_raw_response_from_reader(
315            request.format,
316            &mut stream,
317            &ProgressInstaller::new(&local_db, progress),
318            request.max_input_size,
319        )?;
320    }
321    Ok(Vec::new())
322}
323
324pub(crate) fn plan_push_git(request: GitPushRequest<'_>) -> Result<GitPushPlan> {
325    let GitPushRequest {
326        git_dir,
327        common_git_dir,
328        format,
329        remote,
330        config,
331        refspecs,
332        force,
333    } = request;
334    let (stream, advertisements, features) = receive_pack_advertisements(remote, format, config)?;
335
336    let local_store = FileRefStore::new(git_dir, format);
337    let local_refs = crate::push::local_push_source_refs(&local_store, format)?;
338    let parsed_refspecs = refspecs
339        .iter()
340        .map(|refspec| parse_refspec(&crate::push::normalize_push_refspec(refspec)))
341        .collect::<Result<Vec<_>>>()?;
342    let mut command_forces = Vec::new();
343    for refspec in &parsed_refspecs {
344        for command in plan_push_commands(
345            format,
346            &local_refs,
347            &advertisements,
348            std::slice::from_ref(refspec),
349        )? {
350            command_forces.push((command, force || refspec.force));
351        }
352    }
353    let commands = command_forces
354        .iter()
355        .map(|(command, _)| command.clone())
356        .collect::<Vec<_>>();
357    if commands.is_empty() {
358        let mut stream = stream;
359        let _ = stream.shutdown(Shutdown::Both);
360        return Ok(GitPushPlan {
361            commands,
362            pack_objects: Vec::new(),
363            stream: None,
364            features,
365            advertisements,
366        });
367    }
368
369    let local_db = FileObjectDatabase::from_git_dir(common_git_dir, format);
370    crate::push::reject_non_fast_forward_pushes(
371        common_git_dir,
372        &local_db,
373        format,
374        &command_forces,
375    )?;
376    Ok(GitPushPlan {
377        commands,
378        pack_objects: Vec::new(),
379        stream: Some(stream),
380        features,
381        advertisements,
382    })
383}
384
385pub(crate) fn plan_push_git_commands(request: GitPushCommandsRequest<'_>) -> Result<GitPushPlan> {
386    let GitPushCommandsRequest {
387        common_git_dir,
388        format,
389        remote,
390        config,
391        command_forces,
392        pack_objects,
393    } = request;
394    let (stream, advertisements, features) = receive_pack_advertisements(remote, format, config)?;
395    let commands = command_forces
396        .iter()
397        .map(|(command, _)| command.clone())
398        .collect::<Vec<_>>();
399    if commands.is_empty() {
400        let mut stream = stream;
401        let _ = stream.shutdown(Shutdown::Both);
402        return Ok(GitPushPlan {
403            commands,
404            pack_objects,
405            stream: None,
406            features,
407            advertisements,
408        });
409    }
410
411    let local_db = FileObjectDatabase::from_git_dir(common_git_dir, format);
412    crate::push::reject_non_fast_forward_pushes(
413        common_git_dir,
414        &local_db,
415        format,
416        &command_forces,
417    )?;
418    Ok(GitPushPlan {
419        commands,
420        pack_objects,
421        stream: Some(stream),
422        features,
423        advertisements,
424    })
425}
426
427pub(crate) fn execute_push_git_plan(
428    request: PushRequest<'_>,
429    mut plan: GitPushPlan,
430) -> Result<PushOutcome> {
431    if plan.commands.is_empty() {
432        return Ok(PushOutcome::default());
433    }
434    let mut stream = plan
435        .stream
436        .take()
437        .ok_or_else(|| GitError::Command("git:// receive-pack stream was not available".into()))?;
438    let commands = plan.commands.clone();
439    let local_db = FileObjectDatabase::from_git_dir(request.common_git_dir, request.format);
440    crate::pack::write_receive_pack_body(
441        &crate::pack::PushPackRequest {
442            local_db: &local_db,
443            format: request.format,
444            commands: &commands,
445            pack_objects: &plan.pack_objects,
446            remote_advertisements: &plan.advertisements,
447            features: &plan.features,
448            options: crate::push::receive_pack_push_options(
449                &plan.features,
450                request.format,
451                request.options.quiet,
452                false,
453                &[],
454            ),
455            thin: request.options.thin.wants_thin(),
456        },
457        &mut stream,
458    )?;
459    stream.flush()?;
460    let _ = stream.shutdown(Shutdown::Write);
461
462    let report = if plan.features.report_status || plan.features.report_status_v2 {
463        let report = crate::push::read_receive_pack_push_report(
464            request.format,
465            &mut stream,
466            plan.features.report_status_v2,
467            plan.features.side_band_64k,
468        )?;
469        crate::push::validate_receive_pack_unpack(&report)?;
470        Some(report)
471    } else {
472        let mut sink = Vec::new();
473        stream.read_to_end(&mut sink)?;
474        None
475    };
476    Ok(PushOutcome {
477        commands,
478        report,
479        remote_progress: Vec::new(),
480    })
481}
482
483fn receive_pack_advertisements(
484    remote: &RemoteUrl,
485    format: ObjectFormat,
486    config: Option<&GitConfig>,
487) -> Result<(GitConnection, Vec<RefAdvertisement>, ReceivePackFeatures)> {
488    let mut stream = connect_git_service(remote, GitService::ReceivePack, None, config)?;
489    let advertisement_set = read_ref_advertisement_set(format, &mut stream)?;
490    let features = advertisement_set
491        .refs
492        .first()
493        .map(|advertisement| parse_receive_pack_features(&advertisement.capabilities))
494        .transpose()?
495        .unwrap_or_default();
496    if let Some(remote_format) = features.object_format {
497        if remote_format != format {
498            return Err(GitError::InvalidObjectId(format!(
499                "remote repository uses {}, local repository uses {}",
500                remote_format.name(),
501                format.name()
502            )));
503        }
504    } else if format != ObjectFormat::Sha1 {
505        return Err(GitError::InvalidObjectId(format!(
506            "remote repository did not advertise object-format for {} push",
507            format.name()
508        )));
509    }
510    Ok((stream, advertisement_set.refs, features))
511}
512
513fn connect_git_service(
514    remote: &RemoteUrl,
515    service: GitService,
516    protocol: Option<ProtocolVersion>,
517    config: Option<&GitConfig>,
518) -> Result<GitConnection> {
519    if remote.transport != RemoteTransport::Git {
520        return Err(GitError::InvalidFormat(
521            "git:// service requires a git remote".into(),
522        ));
523    }
524    let host = remote
525        .host
526        .as_deref()
527        .ok_or_else(|| GitError::InvalidFormat("git:// remote is missing a host".into()))?;
528    let port = remote.port.unwrap_or(GIT_DAEMON_PORT);
529    let mut stream = crate::git_proxy::connect_git_transport(config, host, port)?;
530    let protocol = protocol.or_else(|| configured_git_protocol_v1(config));
531    let request = ServiceRequest {
532        service,
533        path: remote.path.clone(),
534        host: Some(git_host_parameter(remote, host, port)),
535        parameters: Vec::new(),
536        protocol,
537        extra_parameters: Vec::new(),
538    };
539    write_service_request(&mut stream, &request)?;
540    stream.flush()?;
541    Ok(stream)
542}
543
544fn configured_git_protocol_v1(config: Option<&GitConfig>) -> Option<ProtocolVersion> {
545    (config.and_then(|config| config.get("protocol", None, "version")) == Some("1"))
546        .then_some(ProtocolVersion::V1)
547}
548
549fn git_protocol_v2_command_options(format: ObjectFormat) -> Vec<Capability> {
550    sley_protocol::encode_protocol_v2_command_options(&ProtocolV2CommandOptions {
551        object_format: Some(format),
552        ..ProtocolV2CommandOptions::default()
553    })
554    .unwrap_or_default()
555}
556
557fn git_protocol_v2_ls_refs(
558    remote: &RemoteUrl,
559    format: ObjectFormat,
560    config: Option<&GitConfig>,
561) -> Result<sley_protocol::RefAdvertisementSet> {
562    let mut stream = connect_git_service(
563        remote,
564        GitService::UploadPack,
565        Some(ProtocolVersion::V2),
566        config,
567    )?;
568    let handshake = read_protocol_v2_advertisement(&mut stream)?;
569    let object_format = protocol_v2_object_format(&handshake.capabilities)?;
570    if object_format != format {
571        return Err(GitError::InvalidObjectId(format!(
572            "remote repository uses {}, local repository uses {}",
573            object_format.name(),
574            format.name()
575        )));
576    }
577    git_protocol_v2_ls_refs_on_stream(format, &mut stream)
578}
579
580fn git_protocol_v2_ls_refs_on_stream(
581    format: ObjectFormat,
582    stream: &mut GitConnection,
583) -> Result<sley_protocol::RefAdvertisementSet> {
584    let mut request = ProtocolV2LsRefsRequest {
585        peel: true,
586        symrefs: true,
587        unborn: true,
588        ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into()],
589    }
590    .to_command_request()?;
591    request.capabilities = git_protocol_v2_command_options(format);
592    write_protocol_v2_command_request(stream, &request)?;
593    stream.flush()?;
594    let records = read_protocol_v2_ls_refs_response(format, stream)?;
595    protocol_v2_ls_refs_records_to_ref_advertisement_set(&records)
596}
597
598fn upload_pack_features_from_v2(
599    handshake: &TransportHandshake,
600    refs: &[RefAdvertisement],
601) -> Result<UploadPackFeatures> {
602    let v2 = parse_protocol_v2_fetch_features(&handshake.capabilities)?.unwrap_or_default();
603    let mut features = UploadPackFeatures {
604        object_format: Some(protocol_v2_object_format(&handshake.capabilities)?),
605        shallow: v2.shallow,
606        deepen_since: v2.shallow,
607        deepen_not: v2.shallow,
608        filter: v2.filter,
609        ..UploadPackFeatures::default()
610    };
611    if let Some(first) = refs.first() {
612        let bridged = parse_upload_pack_features(&first.capabilities)?;
613        features.symrefs = bridged.symrefs;
614    }
615    Ok(features)
616}
617
618fn git_protocol_v2_fetch_into_repository(
619    request: &GitFetchPackRequest<'_>,
620    haves: Vec<ObjectId>,
621    local_db: &FileObjectDatabase,
622    progress: &mut dyn ProgressSink,
623) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
624    let mut stream = connect_git_service(
625        request.remote,
626        GitService::UploadPack,
627        Some(ProtocolVersion::V2),
628        request.config,
629    )?;
630    let handshake = read_protocol_v2_advertisement(&mut stream)?;
631    let v2_features =
632        parse_protocol_v2_fetch_features(&handshake.capabilities)?.unwrap_or_default();
633    let fetch = ProtocolV2FetchRequest {
634        wants: request.wants.clone(),
635        haves,
636        shallow: request.shallow.clone(),
637        deepen: request.deepen,
638        thin_pack: true,
639        include_tag: true,
640        ofs_delta: true,
641        done: true,
642        wait_for_done: v2_features.wait_for_done,
643        sideband_all: v2_features.sideband_all,
644        ..ProtocolV2FetchRequest::default()
645    };
646    write_protocol_v2_fetch_request(&mut stream, &fetch)?;
647    stream.flush()?;
648    let _ = stream.shutdown(Shutdown::Write);
649    let (header, _install) = if request.promisor {
650        install_protocol_v2_fetch_promisor_response_from_reader(
651            request.format,
652            &mut stream,
653            v2_features.sideband_all,
654            local_db,
655            request.max_input_size,
656        )?
657    } else {
658        install_protocol_v2_fetch_response_from_reader(
659            request.format,
660            &mut stream,
661            v2_features.sideband_all,
662            &ProgressInstaller::new(local_db, progress),
663            request.max_input_size,
664        )?
665    };
666    Ok(shallow_info_from_protocol_v2_fetch_header(&header))
667}
668
669fn git_host_parameter(remote: &RemoteUrl, host: &str, port: u16) -> String {
670    match remote.port {
671        Some(_) if host.contains(':') && !host.starts_with('[') => format!("[{host}]:{port}"),
672        Some(_) => format!("{host}:{port}"),
673        None => host.to_string(),
674    }
675}
676
677fn shallow_request_capabilities(deepen: Option<u32>) -> Vec<Capability> {
678    if deepen.is_some() {
679        vec![Capability {
680            name: "shallow".into(),
681            value: None,
682        }]
683    } else {
684        Vec::new()
685    }
686}
687
688fn all_wants_present(db: &FileObjectDatabase, wants: &[ObjectId]) -> Result<bool> {
689    for want in wants {
690        if !db.contains(want)? {
691            return Ok(false);
692        }
693    }
694    Ok(true)
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use std::net::TcpListener;
701    use std::thread;
702
703    use sley_protocol::{ProtocolVersion, RefAdvertisement, RefAdvertisementSet};
704    use sley_transport::read_service_request;
705
706    #[test]
707    fn configured_protocol_v1_is_added_to_git_daemon_requests() {
708        let config = GitConfig::parse(b"[protocol]\n\tversion = 1\n").expect("config");
709        assert_eq!(
710            configured_git_protocol_v1(Some(&config)),
711            Some(ProtocolVersion::V1)
712        );
713        let config = GitConfig::parse(b"[protocol]\n\tversion = 2\n").expect("config");
714        assert_eq!(configured_git_protocol_v1(Some(&config)), None);
715        assert_eq!(configured_git_protocol_v1(None), None);
716    }
717
718    #[test]
719    fn ls_remote_git_sends_daemon_request_and_reads_advertisements() {
720        let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind daemon");
721        let port = listener.local_addr().expect("local addr").port();
722        let tip = ObjectId::from_hex(
723            ObjectFormat::Sha1,
724            "1111111111111111111111111111111111111111",
725        )
726        .expect("oid");
727        let server = thread::spawn(move || {
728            let (mut stream, _) = listener.accept().expect("accept daemon client");
729            let request = read_service_request(&mut stream).expect("service request");
730            assert_eq!(request.service, GitService::UploadPack);
731            assert_eq!(request.path, "/repo.git");
732            let expected_host = format!("127.0.0.1:{port}");
733            assert_eq!(request.host.as_deref(), Some(expected_host.as_str()));
734            sley_protocol::write_ref_advertisement_set(
735                &mut stream,
736                &RefAdvertisementSet {
737                    protocol: ProtocolVersion::V0,
738                    refs: vec![
739                        RefAdvertisement {
740                            oid: tip,
741                            name: "HEAD".into(),
742                            capabilities: vec![Capability {
743                                name: "symref".into(),
744                                value: Some("HEAD:refs/heads/main".into()),
745                            }],
746                        },
747                        RefAdvertisement {
748                            oid: tip,
749                            name: "refs/heads/main".into(),
750                            capabilities: Vec::new(),
751                        },
752                    ],
753                    shallow: Vec::new(),
754                },
755            )
756            .expect("write advertisements");
757        });
758        let remote = RemoteUrl {
759            transport: RemoteTransport::Git,
760            user: None,
761            password: None,
762            host: Some("127.0.0.1".into()),
763            port: Some(port),
764            path: "/repo.git".into(),
765        };
766        let (records, format) = ls_remote_git(
767            &remote,
768            &crate::ls_remote::LsRemoteFilter::default(),
769            &|_| true,
770            false,
771            None,
772        )
773        .expect("ls-remote");
774
775        server.join().expect("server thread");
776        assert_eq!(format, ObjectFormat::Sha1);
777        assert_eq!(records.len(), 2);
778        assert_eq!(records[0].name, "HEAD");
779        assert_eq!(records[0].symref.as_deref(), Some("refs/heads/main"));
780        assert_eq!(records[1].name, "refs/heads/main");
781        assert_eq!(records[1].oid, tip);
782    }
783}