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