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 if conn.protocol_version() >= 2 {
92 return Err(Error::Message(
93 "push_remote: protocol v2 not supported in this phase (use v0/v1)".to_owned(),
94 ));
95 }
96
97 let local_odb = open_odb(local_git_dir);
98 let algo = local_odb.hash_algo();
99
100 let adv = AdvertisedState::from_connection(conn);
103
104 require_push_options_supported(&adv, opts)?;
107
108 let mut plan = match plan_push(refs, &local_odb, local_git_dir, &adv, opts)? {
111 PlanOutcome::Send(plan) => plan,
112 PlanOutcome::Done(results) => return Ok(PushOutcome { results }),
113 };
114
115 let commands = build_command_block(&plan, &adv, algo, &opts.push_options)?;
121 conn.writer().write_all(&commands)?;
122 conn.writer().flush()?;
123
124 if let Some(pack) = build_push_pack(&plan, &local_odb, &adv)? {
125 conn.writer().write_all(&pack)?;
126 conn.writer().flush()?;
127 }
128
129 let mut raw = Vec::new();
143 conn.reader().read_to_end(&mut raw)?;
144 let report = if adv.server_sideband {
145 demux_report_and_remote_messages(&raw, progress)?
146 } else {
147 raw
148 };
149
150 apply_report_status(&report, &mut plan.decisions);
151
152 Ok(PushOutcome {
153 results: plan.decisions.into_iter().map(|d| d.result).collect(),
154 })
155}
156
157pub fn push_http(
187 client: &dyn crate::transport::http::HttpClient,
188 local_git_dir: &Path,
189 repo_url: &str,
190 refs: &[PushRefSpec],
191 opts: &PushOptions,
192 progress: &mut dyn Progress,
193) -> Result<PushOutcome> {
194 let local_odb = open_odb(local_git_dir);
195 let algo = local_odb.hash_algo();
196
197 let adv = discover_receive_pack(client, repo_url)?;
199 if adv.protocol_version >= 2 {
200 return Err(Error::Message(
201 "push_http: protocol v2 receive-pack not supported in this phase (use v0/v1)"
202 .to_owned(),
203 ));
204 }
205
206 require_push_options_supported(&adv.state, opts)?;
209
210 let mut plan = match plan_push(refs, &local_odb, local_git_dir, &adv.state, opts)? {
212 PlanOutcome::Send(plan) => plan,
213 PlanOutcome::Done(results) => return Ok(PushOutcome { results }),
214 };
215
216 let mut body = build_command_block(&plan, &adv.state, algo, &opts.push_options)?;
220 if let Some(pack) = build_push_pack(&plan, &local_odb, &adv.state)? {
221 body.extend_from_slice(&pack);
222 }
223
224 let service_url = receive_pack_url(repo_url);
226 let content_type = format!("application/x-{RECEIVE_PACK}-request");
227 let accept = format!("application/x-{RECEIVE_PACK}-result");
228 let resp = client.post(&service_url, &content_type, &accept, &body, None)?;
229
230 let report = if adv.state.server_sideband {
231 demux_report_and_remote_messages(&resp, progress)?
232 } else {
233 resp
234 };
235
236 apply_report_status(&report, &mut plan.decisions);
237
238 Ok(PushOutcome {
239 results: plan.decisions.into_iter().map(|d| d.result).collect(),
240 })
241}
242
243const RECEIVE_PACK: &str = "git-receive-pack";
244
245struct AdvertisedState {
250 remote_refs: HashMap<String, ObjectId>,
252 advertised_haves: Vec<ObjectId>,
254 server_sideband: bool,
256 server_ofs_delta: bool,
258 server_push_options: bool,
260}
261
262impl AdvertisedState {
263 fn from_connection(conn: &mut dyn Connection) -> Self {
267 let mut remote_refs: HashMap<String, ObjectId> = HashMap::new();
268 let mut advertised_haves: Vec<ObjectId> = Vec::new();
269 for (name, oid) in conn.advertised_refs() {
270 if name == ".have" {
271 advertised_haves.push(*oid);
272 } else {
273 remote_refs.insert(name.clone(), *oid);
274 }
275 }
276 let caps = conn.capabilities();
277 Self {
278 remote_refs,
279 advertised_haves,
280 server_sideband: caps
281 .iter()
282 .any(|c| c == "side-band-64k" || c == "side-band"),
283 server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
284 server_push_options: caps.iter().any(|c| c == "push-options"),
285 }
286 }
287}
288
289struct ReceivePackAdvertisement {
292 protocol_version: u8,
293 state: AdvertisedState,
294}
295
296fn discover_receive_pack(
306 client: &dyn crate::transport::http::HttpClient,
307 repo_url: &str,
308) -> Result<ReceivePackAdvertisement> {
309 let base = repo_url.trim_end_matches('/');
310 let mut refs_url = format!("{base}/info/refs");
311 refs_url.push_str(if refs_url.contains('?') { "&" } else { "?" });
312 refs_url.push_str("service=");
313 refs_url.push_str(RECEIVE_PACK);
314
315 let body = client.get(&refs_url, None)?;
316 let pkt_body = strip_service_advertisement(&body)?;
317 parse_receive_pack_advertisement(pkt_body)
318}
319
320fn receive_pack_url(repo_url: &str) -> String {
322 let base = repo_url.trim_end_matches('/');
323 format!("{base}/{RECEIVE_PACK}")
324}
325
326fn strip_service_advertisement(body: &[u8]) -> Result<&[u8]> {
330 let mut cur = Cursor::new(body);
331 match pkt_line::read_packet(&mut cur)? {
332 Some(Packet::Data(line)) if line.starts_with("# service=") => {
333 match pkt_line::read_packet(&mut cur)? {
334 Some(Packet::Flush) | None => {}
335 _ => return Ok(body),
336 }
337 let pos = cur.position() as usize;
338 Ok(&body[pos..])
339 }
340 _ => Ok(body),
341 }
342}
343
344fn parse_receive_pack_advertisement(body: &[u8]) -> Result<ReceivePackAdvertisement> {
347 let mut cur = Cursor::new(body);
348
349 let first = match pkt_line::read_packet(&mut cur)? {
351 None | Some(Packet::Flush) => {
352 return Ok(ReceivePackAdvertisement {
353 protocol_version: 0,
354 state: AdvertisedState {
355 remote_refs: HashMap::new(),
356 advertised_haves: Vec::new(),
357 server_sideband: false,
358 server_ofs_delta: false,
359 server_push_options: false,
360 },
361 });
362 }
363 Some(Packet::Data(s)) => s,
364 Some(other) => {
365 return Err(Error::Message(format!(
366 "unexpected first receive-pack advertisement packet: {other:?}"
367 )))
368 }
369 };
370 if first.trim_end() == "version 2" {
371 let mut caps: HashSet<String> = HashSet::new();
374 loop {
375 match pkt_line::read_packet(&mut cur)? {
376 None | Some(Packet::Flush) => break,
377 Some(Packet::Data(s)) => {
378 caps.insert(s.trim_end().to_owned());
379 }
380 Some(_) => break,
381 }
382 }
383 return Ok(ReceivePackAdvertisement {
384 protocol_version: 2,
385 state: AdvertisedState {
386 remote_refs: HashMap::new(),
387 advertised_haves: Vec::new(),
388 server_sideband: caps
389 .iter()
390 .any(|c| c == "side-band-64k" || c == "side-band"),
391 server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
392 server_push_options: caps.iter().any(|c| c == "push-options"),
393 },
394 });
395 }
396
397 cur.set_position(0);
399 let mut remote_refs: HashMap<String, ObjectId> = HashMap::new();
400 let mut advertised_haves: Vec<ObjectId> = Vec::new();
401 let mut caps: HashSet<String> = HashSet::new();
402 let mut first_ref_line = true;
403 let mut protocol_version = 0u8;
404 loop {
405 match pkt_line::read_packet(&mut cur)? {
406 None | Some(Packet::Flush) => break,
407 Some(Packet::Data(line)) => {
408 let line = line.trim_end_matches('\n');
409 if line == "version 1" {
410 protocol_version = 1;
411 continue;
412 }
413 if line.starts_with("version ") || line.starts_with("shallow ") {
414 continue;
415 }
416 let (payload, cap_part) = match line.split_once('\0') {
417 Some((p, c)) => (p.trim(), Some(c)),
418 None => (line.trim(), None),
419 };
420 let Some((oid_hex, refname)) =
421 payload.split_once('\t').or_else(|| payload.split_once(' '))
422 else {
423 continue;
424 };
425 let oid_hex = oid_hex.trim();
426 let refname = refname.trim();
427 if first_ref_line {
428 if let Some(raw_caps) = cap_part {
429 for cap in raw_caps.split_whitespace() {
430 caps.insert(cap.to_owned());
431 }
432 }
433 first_ref_line = false;
434 }
435 if refname.is_empty() {
436 continue;
437 }
438 if oid_hex.bytes().all(|b| b == b'0') {
440 continue;
441 }
442 let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
443 Error::Message(format!("bad oid in receive-pack advertisement: {oid_hex}: {e}"))
444 })?;
445 if refname == ".have" {
446 advertised_haves.push(oid);
447 } else {
448 remote_refs.insert(refname.to_owned(), oid);
449 }
450 }
451 Some(other) => {
452 return Err(Error::Message(format!(
453 "unexpected packet in receive-pack advertisement: {other:?}"
454 )))
455 }
456 }
457 }
458 Ok(ReceivePackAdvertisement {
459 protocol_version,
460 state: AdvertisedState {
461 remote_refs,
462 advertised_haves,
463 server_sideband: caps
464 .iter()
465 .any(|c| c == "side-band-64k" || c == "side-band"),
466 server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
467 server_push_options: caps.iter().any(|c| c == "push-options"),
468 },
469 })
470}
471
472struct PushPlan {
475 decisions: Vec<PushDecision>,
476 to_send: Vec<usize>,
478}
479
480enum PlanOutcome {
484 Send(PushPlan),
485 Done(Vec<PushRefResult>),
486}
487
488fn plan_push(
493 refs: &[PushRefSpec],
494 local_odb: &crate::odb::Odb,
495 local_git_dir: &Path,
496 adv: &AdvertisedState,
497 opts: &PushOptions,
498) -> Result<PlanOutcome> {
499 let local_repo = crate::repo::Repository::open(local_git_dir, None).ok();
500
501 let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
502 for spec in refs {
503 decisions.push(decide_push_wire(
504 spec,
505 local_odb,
506 &adv.remote_refs,
507 local_repo.as_ref(),
508 )?);
509 }
510
511 let any_rejected = decisions.iter().any(|d| d.result.status.is_error());
514 if opts.atomic && any_rejected {
515 for d in &mut decisions {
516 if matches!(d.result.status, PushRefStatus::Ok) {
517 d.result.status = PushRefStatus::AtomicPushFailed;
518 d.send = false;
519 }
520 }
521 return Ok(PlanOutcome::Done(
522 decisions.into_iter().map(|d| d.result).collect(),
523 ));
524 }
525
526 let to_send: Vec<usize> = decisions
527 .iter()
528 .enumerate()
529 .filter_map(|(i, d)| if d.send { Some(i) } else { None })
530 .collect();
531
532 if to_send.is_empty() || opts.dry_run {
534 return Ok(PlanOutcome::Done(
535 decisions.into_iter().map(|d| d.result).collect(),
536 ));
537 }
538
539 Ok(PlanOutcome::Send(PushPlan { decisions, to_send }))
540}
541
542fn require_push_options_supported(adv: &AdvertisedState, opts: &PushOptions) -> Result<()> {
550 if !opts.push_options.is_empty() && !adv.server_push_options {
551 return Err(Error::PushOptionsUnsupported);
552 }
553 Ok(())
554}
555
556fn build_command_block(
567 plan: &PushPlan,
568 adv: &AdvertisedState,
569 algo: HashAlgo,
570 push_options: &[String],
571) -> Result<Vec<u8>> {
572 let zero_hex = "0".repeat(algo.hex_len());
573 let mut command_caps = PUSH_CAPS_BASE.to_owned();
574 if adv.server_sideband {
575 command_caps.push_str(" side-band-64k");
576 }
577 if !push_options.is_empty() {
578 command_caps.push_str(" push-options");
579 }
580 command_caps.push_str(&format!(" object-format={}", algo.name()));
581
582 let mut commands: Vec<u8> = Vec::new();
583 let mut first = true;
584 for &i in &plan.to_send {
585 let d = &plan.decisions[i];
586 let old_hex = d
587 .result
588 .old_oid
589 .map(|o| o.to_hex())
590 .unwrap_or_else(|| zero_hex.clone());
591 let new_hex = d
592 .result
593 .new_oid
594 .map(|o| o.to_hex())
595 .unwrap_or_else(|| zero_hex.clone());
596 let line = if first {
602 first = false;
603 format!("{old_hex} {new_hex} {}\0{command_caps}", d.result.remote_ref)
604 } else {
605 format!("{old_hex} {new_hex} {}", d.result.remote_ref)
606 };
607 pkt_line::write_line_to_vec(&mut commands, &line)?;
608 }
609 commands.extend_from_slice(b"0000");
613 if !push_options.is_empty() {
614 for opt in push_options {
615 pkt_line::write_line_to_vec(&mut commands, opt)?;
616 }
617 commands.extend_from_slice(b"0000");
618 }
619 Ok(commands)
620}
621
622fn build_push_pack(
634 plan: &PushPlan,
635 local_odb: &crate::odb::Odb,
636 adv: &AdvertisedState,
637) -> Result<Option<Vec<u8>>> {
638 let wants: Vec<ObjectId> = plan
639 .to_send
640 .iter()
641 .filter_map(|&i| plan.decisions[i].new_tip)
642 .collect();
643
644 if wants.is_empty() {
645 return Ok(None);
646 }
647
648 let mut haves: Vec<ObjectId> = adv.remote_refs.values().copied().collect();
649 haves.extend_from_slice(&adv.advertised_haves);
650 build_pack(
655 local_odb,
656 &wants,
657 &haves,
658 &PackBuildOptions {
659 thin: true,
660 delta: true,
661 use_ofs_delta: adv.server_ofs_delta,
662 ..PackBuildOptions::default()
663 },
664 )
665 .map(Some)
666}
667
668struct PushDecision {
670 result: PushRefResult,
671 new_tip: Option<ObjectId>,
673 send: bool,
675}
676
677fn decide_push_wire(
682 spec: &PushRefSpec,
683 local_odb: &crate::odb::Odb,
684 remote_refs: &HashMap<String, ObjectId>,
685 local_repo: Option<&crate::repo::Repository>,
686) -> Result<PushDecision> {
687 let remote_current = remote_refs.get(&spec.dst).copied();
688
689 let no_op = |status: PushRefStatus,
690 old: Option<ObjectId>,
691 new: Option<ObjectId>,
692 deletion: bool,
693 message: Option<String>| {
694 PushDecision {
695 result: PushRefResult {
696 local_ref: None,
697 remote_ref: spec.dst.clone(),
698 old_oid: old,
699 new_oid: new,
700 forced: false,
701 deletion,
702 status,
703 message,
704 },
705 new_tip: None,
706 send: false,
707 }
708 };
709
710 if !spec.delete {
713 if let Some(src) = spec.src {
714 if remote_current == Some(src) {
715 return Ok(no_op(
716 PushRefStatus::UpToDate,
717 remote_current,
718 Some(src),
719 false,
720 None,
721 ));
722 }
723 }
724 }
725
726 if spec.expect_absent && remote_current.is_some() {
728 return Ok(no_op(
729 PushRefStatus::RejectStale,
730 remote_current,
731 spec.src,
732 spec.delete,
733 Some("stale info".to_owned()),
734 ));
735 }
736
737 if let Some(expected) = spec.expected_old {
739 if remote_current != Some(expected) {
740 return Ok(no_op(
741 PushRefStatus::RejectStale,
742 remote_current,
743 spec.src,
744 spec.delete,
745 Some("stale info".to_owned()),
746 ));
747 }
748 }
749
750 if spec.delete {
751 return Ok(match remote_current {
754 Some(_) => PushDecision {
755 result: PushRefResult {
756 local_ref: None,
757 remote_ref: spec.dst.clone(),
758 old_oid: remote_current,
759 new_oid: None,
760 forced: false,
761 deletion: true,
762 status: PushRefStatus::Ok,
763 message: None,
764 },
765 new_tip: None,
766 send: true,
767 },
768 None => no_op(PushRefStatus::UpToDate, None, None, true, None),
769 });
770 }
771
772 let Some(src) = spec.src else {
773 return Err(Error::Message(format!(
774 "push to '{}' has no source object and is not a deletion",
775 spec.dst
776 )));
777 };
778 if !local_odb.exists(&src) {
779 return Err(Error::Message(format!(
780 "source object {src} for '{}' is missing from the local object store",
781 spec.dst
782 )));
783 }
784
785 let Some(old) = remote_current else {
787 return Ok(PushDecision {
788 result: PushRefResult {
789 local_ref: None,
790 remote_ref: spec.dst.clone(),
791 old_oid: None,
792 new_oid: Some(src),
793 forced: false,
794 deletion: false,
795 status: PushRefStatus::Ok,
796 message: None,
797 },
798 new_tip: Some(src),
799 send: true,
800 });
801 };
802
803 let is_ff = local_repo
806 .map(|r| crate::merge_base::is_ancestor(r, old, src).unwrap_or(false))
807 .unwrap_or(false);
808
809 if is_ff {
810 Ok(PushDecision {
811 result: PushRefResult {
812 local_ref: None,
813 remote_ref: spec.dst.clone(),
814 old_oid: Some(old),
815 new_oid: Some(src),
816 forced: false,
817 deletion: false,
818 status: PushRefStatus::Ok,
819 message: None,
820 },
821 new_tip: Some(src),
822 send: true,
823 })
824 } else if spec.force {
825 Ok(PushDecision {
826 result: PushRefResult {
827 local_ref: None,
828 remote_ref: spec.dst.clone(),
829 old_oid: Some(old),
830 new_oid: Some(src),
831 forced: true,
832 deletion: false,
833 status: PushRefStatus::Ok,
834 message: None,
835 },
836 new_tip: Some(src),
837 send: true,
838 })
839 } else {
840 Ok(PushDecision {
841 result: PushRefResult {
842 local_ref: None,
843 remote_ref: spec.dst.clone(),
844 old_oid: Some(old),
845 new_oid: Some(src),
846 forced: false,
847 deletion: false,
848 status: PushRefStatus::RejectNonFastForward,
849 message: Some("non-fast-forward".to_owned()),
850 },
851 new_tip: None,
852 send: false,
853 })
854 }
855}
856
857fn apply_report_status(report: &[u8], decisions: &mut [PushDecision]) {
871 let mut by_ref: HashMap<&str, usize> = HashMap::new();
872 for (i, d) in decisions.iter().enumerate() {
873 if d.send {
874 by_ref.insert(d.result.remote_ref.as_str(), i);
875 }
876 }
877 let mut unpack_error: Option<String> = None;
879 let mut updates: Vec<(usize, Option<String>)> = Vec::new();
880
881 let mut cursor = Cursor::new(report);
882 while let Ok(Some(pkt)) = pkt_line::read_packet(&mut cursor) {
883 let Packet::Data(line) = pkt else {
884 continue;
885 };
886 let line = line.trim_end();
887 if let Some(rest) = line.strip_prefix("unpack ") {
888 if rest.trim() != "ok" {
889 unpack_error = Some(rest.trim().to_owned());
890 }
891 } else if let Some(refname) = line.strip_prefix("ok ") {
892 let _ = by_ref.get(refname.trim());
894 } else if let Some(rest) = line.strip_prefix("ng ") {
895 let (refname, reason) = rest.split_once(' ').unwrap_or((rest, ""));
897 if let Some(&idx) = by_ref.get(refname.trim()) {
898 let msg = if reason.trim().is_empty() {
899 None
900 } else {
901 Some(reason.trim().to_owned())
902 };
903 updates.push((idx, msg));
904 }
905 }
906 }
907
908 for (idx, msg) in updates {
909 decisions[idx].result.status = PushRefStatus::RemoteRejected;
910 decisions[idx].result.message = msg;
911 }
912
913 if let Some(reason) = unpack_error {
916 for d in decisions.iter_mut() {
917 if d.send && !matches!(d.result.status, PushRefStatus::RemoteRejected) {
918 d.result.status = PushRefStatus::RemoteRejected;
919 d.result.message = Some(format!("unpack failed: {reason}"));
920 }
921 }
922 }
923}
924
925fn demux_report_and_remote_messages(
930 input: &[u8],
931 progress: &mut dyn Progress,
932) -> Result<Vec<u8>> {
933 let mut report = Vec::new();
934 let mut i = 0usize;
935 while i + 4 <= input.len() {
936 let len = match pkt_line::parse_hex_len(&input[i..i + 4]) {
937 Ok(l) => l,
938 Err(_) => break,
939 };
940 i += 4;
941 if len == 0 {
942 continue;
944 }
945 if len < 4 || i + (len - 4) > input.len() {
946 break;
947 }
948 let payload = &input[i..i + (len - 4)];
949 i += len - 4;
950 if payload.is_empty() {
951 continue;
952 }
953 let band = payload[0];
954 let data = &payload[1..];
955 match band {
956 1 => report.extend_from_slice(data),
957 2 | 3 => progress.message(data),
958 _ => {}
959 }
960 }
961 Ok(report)
962}
963
964#[allow(dead_code)]
970fn peel_to_commit(odb: &crate::odb::Odb, oid: ObjectId) -> Option<ObjectId> {
971 let mut current = oid;
972 for _ in 0..16 {
973 let obj = odb.read(¤t).ok()?;
974 match obj.kind {
975 ObjectKind::Commit => return Some(current),
976 ObjectKind::Tag => current = parse_tag(&obj.data).ok()?.object,
977 _ => return None,
978 }
979 }
980 None
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986
987 fn make_decision(refname: &str, send: bool) -> PushDecision {
988 PushDecision {
989 result: PushRefResult {
990 local_ref: None,
991 remote_ref: refname.to_owned(),
992 old_oid: None,
993 new_oid: None,
994 forced: false,
995 deletion: false,
996 status: PushRefStatus::Ok,
997 message: None,
998 },
999 new_tip: None,
1000 send,
1001 }
1002 }
1003
1004 fn report_bytes(lines: &[&str]) -> Vec<u8> {
1005 let mut buf = Vec::new();
1006 for l in lines {
1007 pkt_line::write_line_to_vec(&mut buf, l).unwrap();
1008 }
1009 buf.extend_from_slice(b"0000");
1010 buf
1011 }
1012
1013 fn adv_state(sideband: bool, ofs_delta: bool, push_options: bool) -> AdvertisedState {
1014 AdvertisedState {
1015 remote_refs: HashMap::new(),
1016 advertised_haves: Vec::new(),
1017 server_sideband: sideband,
1018 server_ofs_delta: ofs_delta,
1019 server_push_options: push_options,
1020 }
1021 }
1022
1023 fn decode_block(block: &[u8]) -> Vec<Option<String>> {
1027 let mut cur = Cursor::new(block);
1028 let mut out = Vec::new();
1029 while let Ok(pkt) = pkt_line::read_packet(&mut cur) {
1030 match pkt {
1031 Some(Packet::Data(s)) => out.push(Some(s.trim_end_matches('\n').to_owned())),
1032 Some(Packet::Flush) => out.push(None),
1033 _ => break,
1034 }
1035 }
1036 out
1037 }
1038
1039 fn send_decision(refname: &str, new_oid: ObjectId) -> PushDecision {
1040 PushDecision {
1041 result: PushRefResult {
1042 local_ref: None,
1043 remote_ref: refname.to_owned(),
1044 old_oid: None,
1045 new_oid: Some(new_oid),
1046 forced: false,
1047 deletion: false,
1048 status: PushRefStatus::Ok,
1049 message: None,
1050 },
1051 new_tip: Some(new_oid),
1052 send: true,
1053 }
1054 }
1055
1056 #[test]
1057 fn command_block_without_push_options_has_no_capability_or_lines() {
1058 let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1059 let plan = PushPlan {
1060 decisions: vec![send_decision("refs/heads/main", new)],
1061 to_send: vec![0],
1062 };
1063 let block =
1064 build_command_block(&plan, &adv_state(false, false, true), HashAlgo::Sha1, &[]).unwrap();
1065 let pkts = decode_block(&block);
1066 assert_eq!(pkts.len(), 2);
1068 let cmd = pkts[0].as_deref().unwrap();
1069 assert!(
1070 cmd.contains("refs/heads/main"),
1071 "first line is the ref command, got {cmd:?}"
1072 );
1073 assert!(
1074 !cmd.contains("push-options"),
1075 "no push-options capability without options, got {cmd:?}"
1076 );
1077 assert_eq!(pkts[1], None, "single trailing flush");
1078 }
1079
1080 #[test]
1081 fn command_block_with_push_options_negotiates_cap_and_emits_lines() {
1082 let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1083 let plan = PushPlan {
1084 decisions: vec![send_decision("refs/heads/main", new)],
1085 to_send: vec![0],
1086 };
1087 let opts = vec!["ci.skip".to_owned(), "reviewer=alice".to_owned()];
1088 let block = build_command_block(
1089 &plan,
1090 &adv_state(true, true, true),
1091 HashAlgo::Sha1,
1092 &opts,
1093 )
1094 .unwrap();
1095 let pkts = decode_block(&block);
1096 assert_eq!(
1098 pkts,
1099 vec![
1100 pkts[0].clone(),
1101 None,
1102 Some("ci.skip".to_owned()),
1103 Some("reviewer=alice".to_owned()),
1104 None,
1105 ],
1106 "push-option lines must follow the command-list flush, then a flush"
1107 );
1108 let cmd = pkts[0].as_deref().unwrap();
1109 assert!(
1110 cmd.contains("push-options"),
1111 "capability list must advertise push-options, got {cmd:?}"
1112 );
1113 assert!(cmd.contains("report-status"));
1115 assert!(cmd.contains("side-band-64k"));
1116 assert!(cmd.contains("object-format=sha1"));
1117 }
1118
1119 #[test]
1120 fn require_push_options_errors_typed_when_server_lacks_capability() {
1121 let opts = PushOptions {
1122 push_options: vec!["x".to_owned()],
1123 ..PushOptions::default()
1124 };
1125 let err = require_push_options_supported(&adv_state(true, true, false), &opts).unwrap_err();
1127 assert!(
1128 matches!(err, Error::PushOptionsUnsupported),
1129 "expected PushOptionsUnsupported, got {err:?}"
1130 );
1131 assert_eq!(
1132 err.to_string(),
1133 "the receiving end does not support push options"
1134 );
1135 require_push_options_supported(&adv_state(true, true, true), &opts).unwrap();
1137 require_push_options_supported(&adv_state(true, true, false), &PushOptions::default())
1139 .unwrap();
1140 }
1141
1142 #[test]
1143 fn receive_pack_url_and_strip_preamble() {
1144 assert_eq!(
1145 receive_pack_url("http://h/r.git/"),
1146 "http://h/r.git/git-receive-pack"
1147 );
1148 let mut tail = Vec::new();
1150 pkt_line::write_line_to_vec(&mut tail, &format!("{} refs/heads/main", "1".repeat(40)))
1151 .unwrap();
1152 tail.extend_from_slice(b"0000");
1153
1154 let mut body = Vec::new();
1155 pkt_line::write_line_to_vec(&mut body, "# service=git-receive-pack\n").unwrap();
1156 body.extend_from_slice(b"0000");
1157 body.extend_from_slice(&tail);
1158 assert_eq!(strip_service_advertisement(&body).unwrap(), tail.as_slice());
1159 assert_eq!(strip_service_advertisement(&tail).unwrap(), tail.as_slice());
1161 }
1162
1163 #[test]
1164 fn parses_v0_receive_pack_advertisement_with_caps_and_have() {
1165 let main = "1".repeat(40);
1166 let have = "2".repeat(40);
1167 let mut body = Vec::new();
1168 pkt_line::write_line_to_vec(
1170 &mut body,
1171 &format!(
1172 "{main} refs/heads/main\0report-status report-status-v2 side-band-64k ofs-delta object-format=sha1"
1173 ),
1174 )
1175 .unwrap();
1176 pkt_line::write_line_to_vec(&mut body, &format!("{have} .have")).unwrap();
1178 body.extend_from_slice(b"0000");
1179
1180 let adv = parse_receive_pack_advertisement(&body).unwrap();
1181 assert_eq!(adv.protocol_version, 0);
1182 assert!(adv.state.server_sideband);
1183 assert!(adv.state.server_ofs_delta);
1184 assert_eq!(
1185 adv.state.remote_refs.get("refs/heads/main").map(|o| o.to_hex()),
1186 Some(main.clone())
1187 );
1188 assert_eq!(adv.state.advertised_haves.len(), 1);
1189 assert_eq!(adv.state.advertised_haves[0].to_hex(), have);
1190 assert!(!adv.state.remote_refs.contains_key(".have"));
1192 }
1193
1194 #[test]
1195 fn parses_empty_repo_capabilities_carrier() {
1196 let zero = "0".repeat(40);
1199 let mut body = Vec::new();
1200 pkt_line::write_line_to_vec(
1201 &mut body,
1202 &format!("{zero} capabilities^{{}}\0report-status delete-refs ofs-delta"),
1203 )
1204 .unwrap();
1205 body.extend_from_slice(b"0000");
1206
1207 let adv = parse_receive_pack_advertisement(&body).unwrap();
1208 assert_eq!(adv.protocol_version, 0);
1209 assert!(adv.state.remote_refs.is_empty());
1210 assert!(adv.state.advertised_haves.is_empty());
1211 assert!(adv.state.server_ofs_delta);
1212 assert!(!adv.state.server_sideband);
1213 }
1214
1215 #[test]
1216 fn detects_v2_receive_pack_advertisement() {
1217 let mut body = Vec::new();
1218 pkt_line::write_line_to_vec(&mut body, "version 2").unwrap();
1219 pkt_line::write_line_to_vec(&mut body, "agent=grit/test").unwrap();
1220 pkt_line::write_line_to_vec(&mut body, "object-format=sha1").unwrap();
1221 body.extend_from_slice(b"0000");
1222 let adv = parse_receive_pack_advertisement(&body).unwrap();
1223 assert_eq!(adv.protocol_version, 2);
1224 }
1225
1226 #[test]
1227 fn report_ng_demotes_to_remote_rejected() {
1228 let mut decisions = vec![
1229 make_decision("refs/heads/main", true),
1230 make_decision("refs/heads/topic", true),
1231 ];
1232 let report = report_bytes(&[
1233 "unpack ok",
1234 "ok refs/heads/main",
1235 "ng refs/heads/topic non-fast-forward",
1236 ]);
1237 apply_report_status(&report, &mut decisions);
1238 assert_eq!(decisions[0].result.status, PushRefStatus::Ok);
1239 assert_eq!(decisions[1].result.status, PushRefStatus::RemoteRejected);
1240 assert_eq!(
1241 decisions[1].result.message.as_deref(),
1242 Some("non-fast-forward")
1243 );
1244 }
1245
1246 #[test]
1247 fn report_unpack_failure_rejects_all_sent() {
1248 let mut decisions = vec![make_decision("refs/heads/main", true)];
1249 let report = report_bytes(&["unpack index-pack abort"]);
1250 apply_report_status(&report, &mut decisions);
1251 assert_eq!(decisions[0].result.status, PushRefStatus::RemoteRejected);
1252 assert!(decisions[0]
1253 .result
1254 .message
1255 .as_deref()
1256 .unwrap()
1257 .starts_with("unpack failed:"));
1258 }
1259
1260 #[test]
1261 fn demux_separates_report_and_progress() {
1262 struct Cap(Vec<u8>);
1263 impl Progress for Cap {
1264 fn message(&mut self, bytes: &[u8]) {
1265 self.0.extend_from_slice(bytes);
1266 }
1267 }
1268 let mut wire = Vec::new();
1270 let mut band1 = vec![1u8];
1271 band1.extend_from_slice(b"unpack ok\n");
1272 pkt_line::write_packet_raw(&mut wire, &band1).unwrap();
1273 let mut band2 = vec![2u8];
1274 band2.extend_from_slice(b"hello from hook\n");
1275 pkt_line::write_packet_raw(&mut wire, &band2).unwrap();
1276 wire.extend_from_slice(b"0000");
1277
1278 let mut cap = Cap(Vec::new());
1279 let report = demux_report_and_remote_messages(&wire, &mut cap).unwrap();
1280 assert_eq!(report, b"unpack ok\n");
1281 assert_eq!(cap.0, b"hello from hook\n");
1282 }
1283}