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