1use 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 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
208pub 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}