1use std::collections::HashMap;
28use std::collections::HashSet;
29use std::io::Cursor;
30use std::path::Path;
31
32use crate::error::{Error, Result};
33use crate::fetch::Progress;
34use crate::objects::{parse_tag, HashAlgo, ObjectId, ObjectKind};
35use crate::pkt_line::{self, Packet};
36use crate::push_report::{PushRefResult, PushRefStatus};
37use crate::transfer::{
38 build_pack, open_odb, PackBuildOptions, PushOptions, PushOutcome, PushRefSpec,
39};
40use crate::transport::Connection;
41
42const PUSH_CAPS_BASE: &str = "report-status report-status-v2 quiet";
47
48pub fn push_remote(
85 local_git_dir: &Path,
86 conn: &mut dyn Connection,
87 refs: &[PushRefSpec],
88 opts: &PushOptions,
89 progress: &mut dyn Progress,
90) -> Result<PushOutcome> {
91 use crate::net_trace::net_trace;
92 net_trace!(
93 "push_remote: begin — {} ref update(s), protocol v{}, {} push-option(s)",
94 refs.len(),
95 conn.protocol_version(),
96 opts.push_options.len()
97 );
98 if conn.protocol_version() >= 2 {
99 return Err(Error::Message(
100 "push_remote: protocol v2 not supported in this phase (use v0/v1)".to_owned(),
101 ));
102 }
103
104 let local_odb = open_odb(local_git_dir);
105 let algo = local_odb.hash_algo();
106
107 let adv = AdvertisedState::from_connection(conn);
110 net_trace!(
111 "push_remote: remote advertised {} ref(s)",
112 adv.remote_refs.len()
113 );
114
115 require_push_options_supported(&adv, opts)?;
118
119 let mut plan = match plan_push(refs, &local_odb, local_git_dir, &adv, opts)? {
122 PlanOutcome::Send(plan) => plan,
123 PlanOutcome::Done(results) => return Ok(PushOutcome { results }),
124 };
125
126 let commands = build_command_block(&plan, &adv, algo, &opts.push_options)?;
132 net_trace!("push_remote: sending {} command(s)…", plan.decisions.len());
133 conn.writer().write_all(&commands)?;
134 conn.writer().flush()?;
135
136 if let Some(pack) = build_push_pack(&plan, &local_odb, &adv)? {
137 net_trace!("push_remote: sending pack ({} bytes)…", pack.len());
138 conn.writer().write_all(&pack)?;
139 conn.writer().flush()?;
140 } else {
141 net_trace!("push_remote: no pack (deletion-only / up-to-date)");
142 }
143
144 let mut raw = Vec::new();
158 conn.reader().read_to_end(&mut raw)?;
159 let report = if adv.server_sideband {
160 demux_report_and_remote_messages(&raw, progress)?
161 } else {
162 raw
163 };
164
165 apply_report_status(&report, &mut plan.decisions);
166
167 let results: Vec<_> = plan.decisions.into_iter().map(|d| d.result).collect();
168 net_trace!("push_remote: done — {} result(s)", results.len());
169 Ok(PushOutcome { results })
170}
171
172pub fn push_http(
202 client: &dyn crate::transport::http::HttpClient,
203 local_git_dir: &Path,
204 repo_url: &str,
205 refs: &[PushRefSpec],
206 opts: &PushOptions,
207 progress: &mut dyn Progress,
208) -> Result<PushOutcome> {
209 use crate::net_trace::net_trace;
210 net_trace!(
211 "push_http: begin — {} ref update(s) to {}, {} push-option(s)",
212 refs.len(),
213 repo_url,
214 opts.push_options.len()
215 );
216 let local_odb = open_odb(local_git_dir);
217 let algo = local_odb.hash_algo();
218
219 let adv = discover_receive_pack(client, repo_url)?;
221 net_trace!(
222 "push_http: remote advertised {} ref(s) (protocol v{})",
223 adv.state.remote_refs.len(),
224 adv.protocol_version
225 );
226 if adv.protocol_version >= 2 {
227 return Err(Error::Message(
228 "push_http: protocol v2 receive-pack not supported in this phase (use v0/v1)"
229 .to_owned(),
230 ));
231 }
232
233 require_push_options_supported(&adv.state, opts)?;
236
237 let mut plan = match plan_push(refs, &local_odb, local_git_dir, &adv.state, opts)? {
239 PlanOutcome::Send(plan) => plan,
240 PlanOutcome::Done(results) => return Ok(PushOutcome { results }),
241 };
242
243 let mut body = build_command_block(&plan, &adv.state, algo, &opts.push_options)?;
247 if let Some(pack) = build_push_pack(&plan, &local_odb, &adv.state)? {
248 body.extend_from_slice(&pack);
249 }
250
251 let service_url = receive_pack_url(repo_url);
253 let content_type = format!("application/x-{RECEIVE_PACK}-request");
254 let accept = format!("application/x-{RECEIVE_PACK}-result");
255 net_trace!(
256 "push_http: POST git-receive-pack ({} command(s), {} body bytes)…",
257 plan.decisions.len(),
258 body.len()
259 );
260 let resp = client.post(&service_url, &content_type, &accept, &body, None)?;
261
262 let report = if adv.state.server_sideband {
263 demux_report_and_remote_messages(&resp, progress)?
264 } else {
265 resp
266 };
267
268 apply_report_status(&report, &mut plan.decisions);
269 net_trace!("push_http: done — {} result(s)", plan.decisions.len());
270
271 Ok(PushOutcome {
272 results: plan.decisions.into_iter().map(|d| d.result).collect(),
273 })
274}
275
276const RECEIVE_PACK: &str = "git-receive-pack";
277
278struct AdvertisedState {
283 remote_refs: HashMap<String, ObjectId>,
285 advertised_haves: Vec<ObjectId>,
287 server_sideband: bool,
289 server_ofs_delta: bool,
291 server_push_options: bool,
293}
294
295impl AdvertisedState {
296 fn from_connection(conn: &mut dyn Connection) -> Self {
300 let mut remote_refs: HashMap<String, ObjectId> = HashMap::new();
301 let mut advertised_haves: Vec<ObjectId> = Vec::new();
302 for (name, oid) in conn.advertised_refs() {
303 if name == ".have" {
304 advertised_haves.push(*oid);
305 } else {
306 remote_refs.insert(name.clone(), *oid);
307 }
308 }
309 let caps = conn.capabilities();
310 Self {
311 remote_refs,
312 advertised_haves,
313 server_sideband: caps
314 .iter()
315 .any(|c| c == "side-band-64k" || c == "side-band"),
316 server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
317 server_push_options: caps.iter().any(|c| c == "push-options"),
318 }
319 }
320}
321
322struct ReceivePackAdvertisement {
325 protocol_version: u8,
326 state: AdvertisedState,
327}
328
329fn discover_receive_pack(
339 client: &dyn crate::transport::http::HttpClient,
340 repo_url: &str,
341) -> Result<ReceivePackAdvertisement> {
342 let base = repo_url.trim_end_matches('/');
343 let mut refs_url = format!("{base}/info/refs");
344 refs_url.push_str(if refs_url.contains('?') { "&" } else { "?" });
345 refs_url.push_str("service=");
346 refs_url.push_str(RECEIVE_PACK);
347
348 let body = client.get(&refs_url, None)?;
349 let pkt_body = strip_service_advertisement(&body)?;
350 parse_receive_pack_advertisement(pkt_body)
351}
352
353fn receive_pack_url(repo_url: &str) -> String {
355 let base = repo_url.trim_end_matches('/');
356 format!("{base}/{RECEIVE_PACK}")
357}
358
359fn strip_service_advertisement(body: &[u8]) -> Result<&[u8]> {
363 let mut cur = Cursor::new(body);
364 match pkt_line::read_packet(&mut cur)? {
365 Some(Packet::Data(line)) if line.starts_with("# service=") => {
366 match pkt_line::read_packet(&mut cur)? {
367 Some(Packet::Flush) | None => {}
368 _ => return Ok(body),
369 }
370 let pos = cur.position() as usize;
371 Ok(&body[pos..])
372 }
373 _ => Ok(body),
374 }
375}
376
377fn parse_receive_pack_advertisement(body: &[u8]) -> Result<ReceivePackAdvertisement> {
380 let mut cur = Cursor::new(body);
381
382 let first = match pkt_line::read_packet(&mut cur)? {
384 None | Some(Packet::Flush) => {
385 return Ok(ReceivePackAdvertisement {
386 protocol_version: 0,
387 state: AdvertisedState {
388 remote_refs: HashMap::new(),
389 advertised_haves: Vec::new(),
390 server_sideband: false,
391 server_ofs_delta: false,
392 server_push_options: false,
393 },
394 });
395 }
396 Some(Packet::Data(s)) => s,
397 Some(other) => {
398 return Err(Error::Message(format!(
399 "unexpected first receive-pack advertisement packet: {other:?}"
400 )))
401 }
402 };
403 if first.trim_end() == "version 2" {
404 let mut caps: HashSet<String> = HashSet::new();
407 loop {
408 match pkt_line::read_packet(&mut cur)? {
409 None | Some(Packet::Flush) => break,
410 Some(Packet::Data(s)) => {
411 caps.insert(s.trim_end().to_owned());
412 }
413 Some(_) => break,
414 }
415 }
416 return Ok(ReceivePackAdvertisement {
417 protocol_version: 2,
418 state: AdvertisedState {
419 remote_refs: HashMap::new(),
420 advertised_haves: Vec::new(),
421 server_sideband: caps
422 .iter()
423 .any(|c| c == "side-band-64k" || c == "side-band"),
424 server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
425 server_push_options: caps.iter().any(|c| c == "push-options"),
426 },
427 });
428 }
429
430 cur.set_position(0);
432 let mut remote_refs: HashMap<String, ObjectId> = HashMap::new();
433 let mut advertised_haves: Vec<ObjectId> = Vec::new();
434 let mut caps: HashSet<String> = HashSet::new();
435 let mut first_ref_line = true;
436 let mut protocol_version = 0u8;
437 loop {
438 match pkt_line::read_packet(&mut cur)? {
439 None | Some(Packet::Flush) => break,
440 Some(Packet::Data(line)) => {
441 let line = line.trim_end_matches('\n');
442 if line == "version 1" {
443 protocol_version = 1;
444 continue;
445 }
446 if line.starts_with("version ") || line.starts_with("shallow ") {
447 continue;
448 }
449 let (payload, cap_part) = match line.split_once('\0') {
450 Some((p, c)) => (p.trim(), Some(c)),
451 None => (line.trim(), None),
452 };
453 let Some((oid_hex, refname)) =
454 payload.split_once('\t').or_else(|| payload.split_once(' '))
455 else {
456 continue;
457 };
458 let oid_hex = oid_hex.trim();
459 let refname = refname.trim();
460 if first_ref_line {
461 if let Some(raw_caps) = cap_part {
462 for cap in raw_caps.split_whitespace() {
463 caps.insert(cap.to_owned());
464 }
465 }
466 first_ref_line = false;
467 }
468 if refname.is_empty() {
469 continue;
470 }
471 if oid_hex.bytes().all(|b| b == b'0') {
473 continue;
474 }
475 let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
476 Error::Message(format!(
477 "bad oid in receive-pack advertisement: {oid_hex}: {e}"
478 ))
479 })?;
480 if refname == ".have" {
481 advertised_haves.push(oid);
482 } else {
483 remote_refs.insert(refname.to_owned(), oid);
484 }
485 }
486 Some(other) => {
487 return Err(Error::Message(format!(
488 "unexpected packet in receive-pack advertisement: {other:?}"
489 )))
490 }
491 }
492 }
493 Ok(ReceivePackAdvertisement {
494 protocol_version,
495 state: AdvertisedState {
496 remote_refs,
497 advertised_haves,
498 server_sideband: caps
499 .iter()
500 .any(|c| c == "side-band-64k" || c == "side-band"),
501 server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
502 server_push_options: caps.iter().any(|c| c == "push-options"),
503 },
504 })
505}
506
507struct PushPlan {
510 decisions: Vec<PushDecision>,
511 to_send: Vec<usize>,
513}
514
515enum PlanOutcome {
519 Send(PushPlan),
520 Done(Vec<PushRefResult>),
521}
522
523fn plan_push(
528 refs: &[PushRefSpec],
529 local_odb: &crate::odb::Odb,
530 local_git_dir: &Path,
531 adv: &AdvertisedState,
532 opts: &PushOptions,
533) -> Result<PlanOutcome> {
534 let local_repo = crate::repo::Repository::open(local_git_dir, None).ok();
535
536 let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
537 for spec in refs {
538 decisions.push(decide_push_wire(
539 spec,
540 local_odb,
541 &adv.remote_refs,
542 local_repo.as_ref(),
543 )?);
544 }
545
546 let any_rejected = decisions.iter().any(|d| d.result.status.is_error());
549 if opts.atomic && any_rejected {
550 for d in &mut decisions {
551 if matches!(d.result.status, PushRefStatus::Ok) {
552 d.result.status = PushRefStatus::AtomicPushFailed;
553 d.send = false;
554 }
555 }
556 return Ok(PlanOutcome::Done(
557 decisions.into_iter().map(|d| d.result).collect(),
558 ));
559 }
560
561 let to_send: Vec<usize> = decisions
562 .iter()
563 .enumerate()
564 .filter_map(|(i, d)| if d.send { Some(i) } else { None })
565 .collect();
566
567 if to_send.is_empty() || opts.dry_run {
569 return Ok(PlanOutcome::Done(
570 decisions.into_iter().map(|d| d.result).collect(),
571 ));
572 }
573
574 Ok(PlanOutcome::Send(PushPlan { decisions, to_send }))
575}
576
577fn require_push_options_supported(adv: &AdvertisedState, opts: &PushOptions) -> Result<()> {
585 if !opts.push_options.is_empty() && !adv.server_push_options {
586 return Err(Error::PushOptionsUnsupported);
587 }
588 Ok(())
589}
590
591fn build_command_block(
602 plan: &PushPlan,
603 adv: &AdvertisedState,
604 algo: HashAlgo,
605 push_options: &[String],
606) -> Result<Vec<u8>> {
607 let zero_hex = "0".repeat(algo.hex_len());
608 let mut command_caps = PUSH_CAPS_BASE.to_owned();
609 if adv.server_sideband {
610 command_caps.push_str(" side-band-64k");
611 }
612 if !push_options.is_empty() {
613 command_caps.push_str(" push-options");
614 }
615 command_caps.push_str(&format!(" object-format={}", algo.name()));
616
617 let mut commands: Vec<u8> = Vec::new();
618 let mut first = true;
619 for &i in &plan.to_send {
620 let d = &plan.decisions[i];
621 let old_hex = d
622 .result
623 .old_oid
624 .map(|o| o.to_hex())
625 .unwrap_or_else(|| zero_hex.clone());
626 let new_hex = d
627 .result
628 .new_oid
629 .map(|o| o.to_hex())
630 .unwrap_or_else(|| zero_hex.clone());
631 let line = if first {
637 first = false;
638 format!(
639 "{old_hex} {new_hex} {}\0{command_caps}",
640 d.result.remote_ref
641 )
642 } else {
643 format!("{old_hex} {new_hex} {}", d.result.remote_ref)
644 };
645 pkt_line::write_line_to_vec(&mut commands, &line)?;
646 }
647 commands.extend_from_slice(b"0000");
651 if !push_options.is_empty() {
652 for opt in push_options {
653 pkt_line::write_line_to_vec(&mut commands, opt)?;
654 }
655 commands.extend_from_slice(b"0000");
656 }
657 Ok(commands)
658}
659
660fn build_push_pack(
672 plan: &PushPlan,
673 local_odb: &crate::odb::Odb,
674 adv: &AdvertisedState,
675) -> Result<Option<Vec<u8>>> {
676 let wants: Vec<ObjectId> = plan
677 .to_send
678 .iter()
679 .filter_map(|&i| plan.decisions[i].new_tip)
680 .collect();
681
682 if wants.is_empty() {
683 return Ok(None);
684 }
685
686 let mut haves: Vec<ObjectId> = adv.remote_refs.values().copied().collect();
687 haves.extend_from_slice(&adv.advertised_haves);
688 build_pack(
693 local_odb,
694 &wants,
695 &haves,
696 &PackBuildOptions {
697 thin: true,
698 delta: true,
699 use_ofs_delta: adv.server_ofs_delta,
700 ..PackBuildOptions::default()
701 },
702 )
703 .map(Some)
704}
705
706struct PushDecision {
708 result: PushRefResult,
709 new_tip: Option<ObjectId>,
711 send: bool,
713}
714
715fn decide_push_wire(
720 spec: &PushRefSpec,
721 local_odb: &crate::odb::Odb,
722 remote_refs: &HashMap<String, ObjectId>,
723 local_repo: Option<&crate::repo::Repository>,
724) -> Result<PushDecision> {
725 let remote_current = remote_refs.get(&spec.dst).copied();
726
727 let no_op = |status: PushRefStatus,
728 old: Option<ObjectId>,
729 new: Option<ObjectId>,
730 deletion: bool,
731 message: Option<String>| {
732 PushDecision {
733 result: PushRefResult {
734 local_ref: None,
735 remote_ref: spec.dst.clone(),
736 old_oid: old,
737 new_oid: new,
738 forced: false,
739 deletion,
740 status,
741 message,
742 },
743 new_tip: None,
744 send: false,
745 }
746 };
747
748 if !spec.delete {
751 if let Some(src) = spec.src {
752 if remote_current == Some(src) {
753 return Ok(no_op(
754 PushRefStatus::UpToDate,
755 remote_current,
756 Some(src),
757 false,
758 None,
759 ));
760 }
761 }
762 }
763
764 if spec.expect_absent && remote_current.is_some() {
766 return Ok(no_op(
767 PushRefStatus::RejectStale,
768 remote_current,
769 spec.src,
770 spec.delete,
771 Some("stale info".to_owned()),
772 ));
773 }
774
775 if let Some(expected) = spec.expected_old {
777 if remote_current != Some(expected) {
778 return Ok(no_op(
779 PushRefStatus::RejectStale,
780 remote_current,
781 spec.src,
782 spec.delete,
783 Some("stale info".to_owned()),
784 ));
785 }
786 }
787
788 if spec.delete {
789 return Ok(match remote_current {
792 Some(_) => PushDecision {
793 result: PushRefResult {
794 local_ref: None,
795 remote_ref: spec.dst.clone(),
796 old_oid: remote_current,
797 new_oid: None,
798 forced: false,
799 deletion: true,
800 status: PushRefStatus::Ok,
801 message: None,
802 },
803 new_tip: None,
804 send: true,
805 },
806 None => no_op(PushRefStatus::UpToDate, None, None, true, None),
807 });
808 }
809
810 let Some(src) = spec.src else {
811 return Err(Error::Message(format!(
812 "push to '{}' has no source object and is not a deletion",
813 spec.dst
814 )));
815 };
816 if !local_odb.exists(&src) {
817 return Err(Error::Message(format!(
818 "source object {src} for '{}' is missing from the local object store",
819 spec.dst
820 )));
821 }
822
823 let Some(old) = remote_current else {
825 return Ok(PushDecision {
826 result: PushRefResult {
827 local_ref: None,
828 remote_ref: spec.dst.clone(),
829 old_oid: None,
830 new_oid: Some(src),
831 forced: false,
832 deletion: false,
833 status: PushRefStatus::Ok,
834 message: None,
835 },
836 new_tip: Some(src),
837 send: true,
838 });
839 };
840
841 let is_ff = local_repo
844 .map(|r| crate::merge_base::is_ancestor(r, old, src).unwrap_or(false))
845 .unwrap_or(false);
846
847 if is_ff {
848 Ok(PushDecision {
849 result: PushRefResult {
850 local_ref: None,
851 remote_ref: spec.dst.clone(),
852 old_oid: Some(old),
853 new_oid: Some(src),
854 forced: false,
855 deletion: false,
856 status: PushRefStatus::Ok,
857 message: None,
858 },
859 new_tip: Some(src),
860 send: true,
861 })
862 } else if spec.force {
863 Ok(PushDecision {
864 result: PushRefResult {
865 local_ref: None,
866 remote_ref: spec.dst.clone(),
867 old_oid: Some(old),
868 new_oid: Some(src),
869 forced: true,
870 deletion: false,
871 status: PushRefStatus::Ok,
872 message: None,
873 },
874 new_tip: Some(src),
875 send: true,
876 })
877 } else {
878 Ok(PushDecision {
879 result: PushRefResult {
880 local_ref: None,
881 remote_ref: spec.dst.clone(),
882 old_oid: Some(old),
883 new_oid: Some(src),
884 forced: false,
885 deletion: false,
886 status: PushRefStatus::RejectNonFastForward,
887 message: Some("non-fast-forward".to_owned()),
888 },
889 new_tip: None,
890 send: false,
891 })
892 }
893}
894
895fn apply_report_status(report: &[u8], decisions: &mut [PushDecision]) {
909 let mut by_ref: HashMap<&str, usize> = HashMap::new();
910 for (i, d) in decisions.iter().enumerate() {
911 if d.send {
912 by_ref.insert(d.result.remote_ref.as_str(), i);
913 }
914 }
915 let mut unpack_error: Option<String> = None;
917 let mut updates: Vec<(usize, Option<String>)> = Vec::new();
918
919 let mut cursor = Cursor::new(report);
920 while let Ok(Some(pkt)) = pkt_line::read_packet(&mut cursor) {
921 let Packet::Data(line) = pkt else {
922 continue;
923 };
924 let line = line.trim_end();
925 if let Some(rest) = line.strip_prefix("unpack ") {
926 if rest.trim() != "ok" {
927 unpack_error = Some(rest.trim().to_owned());
928 }
929 } else if let Some(refname) = line.strip_prefix("ok ") {
930 let _ = by_ref.get(refname.trim());
932 } else if let Some(rest) = line.strip_prefix("ng ") {
933 let (refname, reason) = rest.split_once(' ').unwrap_or((rest, ""));
935 if let Some(&idx) = by_ref.get(refname.trim()) {
936 let msg = if reason.trim().is_empty() {
937 None
938 } else {
939 Some(reason.trim().to_owned())
940 };
941 updates.push((idx, msg));
942 }
943 }
944 }
945
946 for (idx, msg) in updates {
947 decisions[idx].result.status = PushRefStatus::RemoteRejected;
948 decisions[idx].result.message = msg;
949 }
950
951 if let Some(reason) = unpack_error {
954 for d in decisions.iter_mut() {
955 if d.send && !matches!(d.result.status, PushRefStatus::RemoteRejected) {
956 d.result.status = PushRefStatus::RemoteRejected;
957 d.result.message = Some(format!("unpack failed: {reason}"));
958 }
959 }
960 }
961}
962
963fn demux_report_and_remote_messages(input: &[u8], progress: &mut dyn Progress) -> Result<Vec<u8>> {
968 let mut report = Vec::new();
969 let mut i = 0usize;
970 while i + 4 <= input.len() {
971 let len = match pkt_line::parse_hex_len(&input[i..i + 4]) {
972 Ok(l) => l,
973 Err(_) => break,
974 };
975 i += 4;
976 if len == 0 {
977 continue;
979 }
980 if len < 4 || i + (len - 4) > input.len() {
981 break;
982 }
983 let payload = &input[i..i + (len - 4)];
984 i += len - 4;
985 if payload.is_empty() {
986 continue;
987 }
988 let band = payload[0];
989 let data = &payload[1..];
990 match band {
991 1 => report.extend_from_slice(data),
992 2 | 3 => progress.message(data),
993 _ => {}
994 }
995 }
996 Ok(report)
997}
998
999#[allow(dead_code)]
1005fn peel_to_commit(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
1006 let mut current = oid;
1007 for _ in 0..16 {
1008 let obj = odb.read(¤t).ok()?;
1009 match obj.kind {
1010 ObjectKind::Commit => return Some(current),
1011 ObjectKind::Tag => current = parse_tag(&obj.data).ok()?.object,
1012 _ => return None,
1013 }
1014 }
1015 None
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021
1022 fn make_decision(refname: &str, send: bool) -> PushDecision {
1023 PushDecision {
1024 result: PushRefResult {
1025 local_ref: None,
1026 remote_ref: refname.to_owned(),
1027 old_oid: None,
1028 new_oid: None,
1029 forced: false,
1030 deletion: false,
1031 status: PushRefStatus::Ok,
1032 message: None,
1033 },
1034 new_tip: None,
1035 send,
1036 }
1037 }
1038
1039 fn report_bytes(lines: &[&str]) -> Vec<u8> {
1040 let mut buf = Vec::new();
1041 for l in lines {
1042 pkt_line::write_line_to_vec(&mut buf, l).unwrap();
1043 }
1044 buf.extend_from_slice(b"0000");
1045 buf
1046 }
1047
1048 fn adv_state(sideband: bool, ofs_delta: bool, push_options: bool) -> AdvertisedState {
1049 AdvertisedState {
1050 remote_refs: HashMap::new(),
1051 advertised_haves: Vec::new(),
1052 server_sideband: sideband,
1053 server_ofs_delta: ofs_delta,
1054 server_push_options: push_options,
1055 }
1056 }
1057
1058 fn decode_block(block: &[u8]) -> Vec<Option<String>> {
1062 let mut cur = Cursor::new(block);
1063 let mut out = Vec::new();
1064 while let Ok(pkt) = pkt_line::read_packet(&mut cur) {
1065 match pkt {
1066 Some(Packet::Data(s)) => out.push(Some(s.trim_end_matches('\n').to_owned())),
1067 Some(Packet::Flush) => out.push(None),
1068 _ => break,
1069 }
1070 }
1071 out
1072 }
1073
1074 fn send_decision(refname: &str, new_oid: ObjectId) -> PushDecision {
1075 PushDecision {
1076 result: PushRefResult {
1077 local_ref: None,
1078 remote_ref: refname.to_owned(),
1079 old_oid: None,
1080 new_oid: Some(new_oid),
1081 forced: false,
1082 deletion: false,
1083 status: PushRefStatus::Ok,
1084 message: None,
1085 },
1086 new_tip: Some(new_oid),
1087 send: true,
1088 }
1089 }
1090
1091 #[test]
1092 fn command_block_without_push_options_has_no_capability_or_lines() {
1093 let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1094 let plan = PushPlan {
1095 decisions: vec![send_decision("refs/heads/main", new)],
1096 to_send: vec![0],
1097 };
1098 let block = build_command_block(&plan, &adv_state(false, false, true), HashAlgo::Sha1, &[])
1099 .unwrap();
1100 let pkts = decode_block(&block);
1101 assert_eq!(pkts.len(), 2);
1103 let cmd = pkts[0].as_deref().unwrap();
1104 assert!(
1105 cmd.contains("refs/heads/main"),
1106 "first line is the ref command, got {cmd:?}"
1107 );
1108 assert!(
1109 !cmd.contains("push-options"),
1110 "no push-options capability without options, got {cmd:?}"
1111 );
1112 assert_eq!(pkts[1], None, "single trailing flush");
1113 }
1114
1115 #[test]
1116 fn command_block_with_push_options_negotiates_cap_and_emits_lines() {
1117 let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1118 let plan = PushPlan {
1119 decisions: vec![send_decision("refs/heads/main", new)],
1120 to_send: vec![0],
1121 };
1122 let opts = vec!["ci.skip".to_owned(), "reviewer=alice".to_owned()];
1123 let block = build_command_block(&plan, &adv_state(true, true, true), HashAlgo::Sha1, &opts)
1124 .unwrap();
1125 let pkts = decode_block(&block);
1126 assert_eq!(
1128 pkts,
1129 vec![
1130 pkts[0].clone(),
1131 None,
1132 Some("ci.skip".to_owned()),
1133 Some("reviewer=alice".to_owned()),
1134 None,
1135 ],
1136 "push-option lines must follow the command-list flush, then a flush"
1137 );
1138 let cmd = pkts[0].as_deref().unwrap();
1139 assert!(
1140 cmd.contains("push-options"),
1141 "capability list must advertise push-options, got {cmd:?}"
1142 );
1143 assert!(cmd.contains("report-status"));
1145 assert!(cmd.contains("side-band-64k"));
1146 assert!(cmd.contains("object-format=sha1"));
1147 }
1148
1149 #[test]
1150 fn require_push_options_errors_typed_when_server_lacks_capability() {
1151 let opts = PushOptions {
1152 push_options: vec!["x".to_owned()],
1153 ..PushOptions::default()
1154 };
1155 let err = require_push_options_supported(&adv_state(true, true, false), &opts).unwrap_err();
1157 assert!(
1158 matches!(err, Error::PushOptionsUnsupported),
1159 "expected PushOptionsUnsupported, got {err:?}"
1160 );
1161 assert_eq!(
1162 err.to_string(),
1163 "the receiving end does not support push options"
1164 );
1165 require_push_options_supported(&adv_state(true, true, true), &opts).unwrap();
1167 require_push_options_supported(&adv_state(true, true, false), &PushOptions::default())
1169 .unwrap();
1170 }
1171
1172 #[test]
1173 fn receive_pack_url_and_strip_preamble() {
1174 assert_eq!(
1175 receive_pack_url("http://h/r.git/"),
1176 "http://h/r.git/git-receive-pack"
1177 );
1178 let mut tail = Vec::new();
1180 pkt_line::write_line_to_vec(&mut tail, &format!("{} refs/heads/main", "1".repeat(40)))
1181 .unwrap();
1182 tail.extend_from_slice(b"0000");
1183
1184 let mut body = Vec::new();
1185 pkt_line::write_line_to_vec(&mut body, "# service=git-receive-pack\n").unwrap();
1186 body.extend_from_slice(b"0000");
1187 body.extend_from_slice(&tail);
1188 assert_eq!(strip_service_advertisement(&body).unwrap(), tail.as_slice());
1189 assert_eq!(strip_service_advertisement(&tail).unwrap(), tail.as_slice());
1191 }
1192
1193 #[test]
1194 fn parses_v0_receive_pack_advertisement_with_caps_and_have() {
1195 let main = "1".repeat(40);
1196 let have = "2".repeat(40);
1197 let mut body = Vec::new();
1198 pkt_line::write_line_to_vec(
1200 &mut body,
1201 &format!(
1202 "{main} refs/heads/main\0report-status report-status-v2 side-band-64k ofs-delta object-format=sha1"
1203 ),
1204 )
1205 .unwrap();
1206 pkt_line::write_line_to_vec(&mut body, &format!("{have} .have")).unwrap();
1208 body.extend_from_slice(b"0000");
1209
1210 let adv = parse_receive_pack_advertisement(&body).unwrap();
1211 assert_eq!(adv.protocol_version, 0);
1212 assert!(adv.state.server_sideband);
1213 assert!(adv.state.server_ofs_delta);
1214 assert_eq!(
1215 adv.state
1216 .remote_refs
1217 .get("refs/heads/main")
1218 .map(|o| o.to_hex()),
1219 Some(main.clone())
1220 );
1221 assert_eq!(adv.state.advertised_haves.len(), 1);
1222 assert_eq!(adv.state.advertised_haves[0].to_hex(), have);
1223 assert!(!adv.state.remote_refs.contains_key(".have"));
1225 }
1226
1227 #[test]
1228 fn parses_empty_repo_capabilities_carrier() {
1229 let zero = "0".repeat(40);
1232 let mut body = Vec::new();
1233 pkt_line::write_line_to_vec(
1234 &mut body,
1235 &format!("{zero} capabilities^{{}}\0report-status delete-refs ofs-delta"),
1236 )
1237 .unwrap();
1238 body.extend_from_slice(b"0000");
1239
1240 let adv = parse_receive_pack_advertisement(&body).unwrap();
1241 assert_eq!(adv.protocol_version, 0);
1242 assert!(adv.state.remote_refs.is_empty());
1243 assert!(adv.state.advertised_haves.is_empty());
1244 assert!(adv.state.server_ofs_delta);
1245 assert!(!adv.state.server_sideband);
1246 }
1247
1248 #[test]
1249 fn detects_v2_receive_pack_advertisement() {
1250 let mut body = Vec::new();
1251 pkt_line::write_line_to_vec(&mut body, "version 2").unwrap();
1252 pkt_line::write_line_to_vec(&mut body, "agent=grit/test").unwrap();
1253 pkt_line::write_line_to_vec(&mut body, "object-format=sha1").unwrap();
1254 body.extend_from_slice(b"0000");
1255 let adv = parse_receive_pack_advertisement(&body).unwrap();
1256 assert_eq!(adv.protocol_version, 2);
1257 }
1258
1259 #[test]
1260 fn report_ng_demotes_to_remote_rejected() {
1261 let mut decisions = vec![
1262 make_decision("refs/heads/main", true),
1263 make_decision("refs/heads/topic", true),
1264 ];
1265 let report = report_bytes(&[
1266 "unpack ok",
1267 "ok refs/heads/main",
1268 "ng refs/heads/topic non-fast-forward",
1269 ]);
1270 apply_report_status(&report, &mut decisions);
1271 assert_eq!(decisions[0].result.status, PushRefStatus::Ok);
1272 assert_eq!(decisions[1].result.status, PushRefStatus::RemoteRejected);
1273 assert_eq!(
1274 decisions[1].result.message.as_deref(),
1275 Some("non-fast-forward")
1276 );
1277 }
1278
1279 #[test]
1280 fn report_unpack_failure_rejects_all_sent() {
1281 let mut decisions = vec![make_decision("refs/heads/main", true)];
1282 let report = report_bytes(&["unpack index-pack abort"]);
1283 apply_report_status(&report, &mut decisions);
1284 assert_eq!(decisions[0].result.status, PushRefStatus::RemoteRejected);
1285 assert!(decisions[0]
1286 .result
1287 .message
1288 .as_deref()
1289 .unwrap()
1290 .starts_with("unpack failed:"));
1291 }
1292
1293 #[test]
1294 fn demux_separates_report_and_progress() {
1295 struct Cap(Vec<u8>);
1296 impl Progress for Cap {
1297 fn message(&mut self, bytes: &[u8]) {
1298 self.0.extend_from_slice(bytes);
1299 }
1300 }
1301 let mut wire = Vec::new();
1303 let mut band1 = vec![1u8];
1304 band1.extend_from_slice(b"unpack ok\n");
1305 pkt_line::write_packet_raw(&mut wire, &band1).unwrap();
1306 let mut band2 = vec![2u8];
1307 band2.extend_from_slice(b"hello from hook\n");
1308 pkt_line::write_packet_raw(&mut wire, &band2).unwrap();
1309 wire.extend_from_slice(b"0000");
1310
1311 let mut cap = Cap(Vec::new());
1312 let report = demux_report_and_remote_messages(&wire, &mut cap).unwrap();
1313 assert_eq!(report, b"unpack ok\n");
1314 assert_eq!(cap.0, b"hello from hook\n");
1315 }
1316}