1use std::collections::{HashMap, HashSet};
28use std::io::{Cursor, ErrorKind, Read, Write};
29use std::path::Path;
30
31use crate::branch_tracking;
32use crate::environment::RepositoryOptions;
33use crate::error::{Error, Result};
34use crate::fetch::Progress;
35use crate::objects::{HashAlgo, ObjectId};
36use crate::pack_objects::PackStats;
37use crate::pkt_line::{self, Packet};
38use crate::progress::{poll_cancel, Cancel, ProgressPhase, ProgressUnit, ReceivedObjects};
39use crate::push_report::{PushRefResult, PushRefStatus};
40use crate::repo::Repository;
41use crate::sideband_lines::{RemoteLine, SidebandLines};
42use crate::transfer::{PackBuildOptions, PrePushGate, PushOptions, PushOutcome, PushRefSpec};
43use crate::transport::Connection;
44
45fn finish_push_outcome(
46 local: &Repository,
47 opts: &PushOptions,
48 results: Vec<PushRefResult>,
49 remote_messages: Vec<String>,
50) -> Result<PushOutcome> {
51 if !opts.dry_run {
52 if let Some(remote) = opts.tracking_remote.as_deref() {
53 branch_tracking::apply_push_remote_tracking_updates_in(local, remote, &results)?;
54 }
55 }
56 Ok(PushOutcome {
57 results,
58 remote_messages,
59 })
60}
61
62struct PushSidebandCollector<'a> {
64 inner: &'a mut dyn Progress,
65 remote_messages: Vec<String>,
66 pending: SidebandLines,
67}
68
69impl PushSidebandCollector<'_> {
70 fn record_remote_chunk(&mut self, bytes: &[u8]) {
71 let mut lines = Vec::new();
72 self.pending.feed(bytes, |line| lines.push(line));
73 for line in lines {
74 self.record_remote_line(line);
75 }
76 }
77
78 fn record_remote_line(&mut self, line: RemoteLine) {
79 if let RemoteLine::Line(text) = line {
80 if !text.is_empty() {
81 self.remote_messages.push(text);
82 }
83 }
84 }
85
86 fn finish_pending(&mut self) {
87 let mut lines = Vec::new();
88 self.pending.finish(|line| lines.push(line));
89 for line in lines {
90 self.record_remote_line(line);
91 }
92 }
93}
94
95impl crate::progress::ProgressSink for PushSidebandCollector<'_> {
96 fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
97 self.inner.start(phase, total);
98 }
99
100 fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
101 self.inner.start_with_unit(phase, unit, total);
102 }
103
104 fn inc(&mut self, units: u64) {
105 self.inner.inc(units);
106 }
107
108 fn set(&mut self, current: u64) {
109 self.inner.set(current);
110 }
111
112 fn finish(&mut self) {
113 self.inner.finish();
114 }
115
116 fn should_cancel(&self) -> bool {
117 self.inner.should_cancel()
118 }
119}
120
121impl Progress for PushSidebandCollector<'_> {
122 fn message(&mut self, bytes: &[u8]) {
123 self.inner.message(bytes);
124 self.record_remote_chunk(bytes);
125 }
126
127 fn remote_sideband_progress(&self) -> bool {
128 self.inner.remote_sideband_progress()
129 }
130
131 fn wants_received_objects(&self) -> bool {
132 self.inner.wants_received_objects()
133 }
134
135 fn received_objects(&mut self, counts: ReceivedObjects) {
136 self.inner.received_objects(counts);
137 }
138}
139
140const PUSH_CAPS_BASE: &str = "report-status report-status-v2 quiet";
145
146pub fn push_remote(
186 local_git_dir: &Path,
187 conn: &mut dyn Connection,
188 refs: &[PushRefSpec],
189 opts: &PushOptions,
190 progress: &mut dyn Progress,
191 cancel: &dyn Cancel,
192) -> Result<PushOutcome> {
193 let local = Repository::open(local_git_dir, &RepositoryOptions::empty())?;
194 push_remote_from(&local, conn, refs, opts, progress, cancel, None)
195}
196
197pub(crate) fn push_remote_from(
201 local: &Repository,
202 conn: &mut dyn Connection,
203 refs: &[PushRefSpec],
204 opts: &PushOptions,
205 progress: &mut dyn Progress,
206 cancel: &dyn Cancel,
207 pre_push: Option<&PrePushGate<'_>>,
208) -> Result<PushOutcome> {
209 push_remote_with(local, conn, refs, opts, progress, cancel, None, pre_push)
210}
211
212pub(crate) struct BundlePushTarget<'a> {
215 pub(crate) client: &'a dyn crate::transport::http::HttpClient,
217 pub(crate) repo_url: &'a str,
219}
220
221#[allow(clippy::too_many_arguments)]
223pub(crate) fn push_remote_with(
224 local: &Repository,
225 conn: &mut dyn Connection,
226 refs: &[PushRefSpec],
227 opts: &PushOptions,
228 progress: &mut dyn Progress,
229 cancel: &dyn Cancel,
230 bundle_target: Option<BundlePushTarget<'_>>,
231 pre_push: Option<&PrePushGate<'_>>,
232) -> Result<PushOutcome> {
233 crate::trace::trace_span!("grit.push");
234 let local_git_dir = local.git_dir();
235 let mut sideband = PushSidebandCollector {
236 inner: progress,
237 remote_messages: Vec::new(),
238 pending: SidebandLines::new(),
239 };
240 poll_cancel(&mut sideband, cancel)?;
241 let stop = crate::progress::CancelSignal::new(cancel);
242 use crate::net_trace::net_trace;
243 net_trace!(
244 opts.network_trace,
245 opts.diagnostics.as_ref(),
246 "push_remote: begin — {} ref update(s), protocol v{}, {} push-option(s)",
247 refs.len(),
248 conn.protocol_version(),
249 opts.push_options.len()
250 );
251 if conn.protocol_version() >= 2 {
252 return Err(Error::Message(
253 "push_remote: protocol v2 not supported in this phase (use v0/v1)".to_owned(),
254 ));
255 }
256
257 let algo = local.odb().hash_algo();
258
259 let adv = AdvertisedState::from_connection(conn);
262 net_trace!(
263 opts.network_trace,
264 opts.diagnostics.as_ref(),
265 "push_remote: remote advertised {} ref(s)",
266 adv.remote_refs.len()
267 );
268
269 require_push_capabilities(&adv, opts)?;
272
273 let planned = plan_push(refs, local, &adv, opts, pre_push);
276 if planned.is_err() && !opts.dry_run {
277 let _ = crate::transport::send_client_flush(conn);
279 }
280 let mut plan = match planned? {
281 PlanOutcome::Send(plan) => plan,
282 PlanOutcome::Done(results) => {
283 if !opts.dry_run {
284 crate::transport::send_client_flush(conn)?;
285 }
286 return finish_push_outcome(local, opts, results, Vec::new());
287 }
288 };
289
290 let commands = build_command_block(&plan, &adv, algo, opts)?;
296 let sent_refs: Vec<&str> = plan
297 .to_send
298 .iter()
299 .map(|&i| plan.decisions[i].result.remote_ref.as_str())
300 .collect();
301
302 let bundle_route = match (&bundle_target, adv.push_bundle.as_deref()) {
306 (Some(target), Some(endpoint)) if !opts.dry_run => {
307 crate::push_bundle::resolve_endpoint(target.repo_url, endpoint)
308 .map(|endpoint| (target, endpoint))
309 }
310 _ => None,
311 };
312 let pack = PushPack::for_plan(&plan, local, &adv)?;
313 let mut spooled_pack = None;
314 if let Some((target, endpoint)) = &bundle_route {
315 let session =
316 crate::push_bundle::Session::locate(local_git_dir, target.repo_url, &commands);
317 let ready = match &pack {
318 _ if session.has_bundle() => true,
319 None => false,
320 Some(pack) => {
321 let threshold = opts
322 .resumable_threshold
323 .unwrap_or(crate::push_bundle::DEFAULT_THRESHOLD);
324 let tips: Vec<(ObjectId, String)> = plan
325 .to_send
326 .iter()
327 .filter_map(|&i| {
328 let d = &plan.decisions[i];
329 Some((d.new_tip?, d.result.remote_ref.clone()))
330 })
331 .collect();
332 let mut haves: Vec<ObjectId> = adv.remote_refs.values().copied().collect();
333 haves.extend_from_slice(&adv.advertised_haves);
334 match session.write_bundle(local.odb(), &haves, &tips, threshold, |w| {
335 pack.write_to(local, w, &stop).map(drop)
336 })? {
337 crate::push_bundle::Spooled::Bundle => true,
338 crate::push_bundle::Spooled::Small(small) => {
339 spooled_pack = Some(small);
340 false
341 }
342 }
343 }
344 };
345 if ready {
346 let raw = crate::push_bundle::push(
347 target.client,
348 &session,
349 endpoint,
350 &commands,
351 &sent_refs,
352 adv.server_sideband,
353 &mut sideband,
354 )?;
355 let report = if adv.server_sideband {
356 demux_report_and_remote_messages(&raw, &mut sideband)?
357 } else {
358 raw
359 };
360 apply_report_status(&report, &mut plan.decisions);
361 let results: Vec<_> = plan.decisions.into_iter().map(|d| d.result).collect();
362 sideband.finish_pending();
363 return finish_push_outcome(local, opts, results, sideband.remote_messages);
364 }
365 }
366
367 net_trace!(
368 opts.network_trace,
369 opts.diagnostics.as_ref(),
370 "push_remote: sending {} command(s)…",
371 plan.decisions.len()
372 );
373 conn.writer().write_all(&commands)?;
374 conn.writer().flush()?;
375
376 if let Some(pack) = &pack {
377 poll_cancel(&mut sideband, cancel)?;
378 net_trace!(
379 opts.network_trace,
380 opts.diagnostics.as_ref(),
381 "push_remote: sending pack…"
382 );
383 let mut out = std::io::BufWriter::with_capacity(PACK_SEND_BUFFER, conn.writer());
386 let sent = match spooled_pack.take() {
387 Some(spooled) => spooled.copy_to(&mut out)?,
388 None => pack.write_to(local, &mut out, &stop)?.pack_bytes,
389 };
390 out.flush()?;
391 drop(out);
392 conn.writer().flush()?;
393 net_trace!(
394 opts.network_trace,
395 opts.diagnostics.as_ref(),
396 "push_remote: sent pack ({sent} bytes)"
397 );
398 } else {
399 net_trace!(
400 opts.network_trace,
401 opts.diagnostics.as_ref(),
402 "push_remote: no pack (deletion-only / up-to-date)"
403 );
404 }
405
406 conn.wait_for_response(cancel)?;
425 let raw = read_receive_pack_response(conn.reader(), &sent_refs, adv.server_sideband, cancel)?;
426 let report = if adv.server_sideband {
427 demux_report_and_remote_messages(&raw, &mut sideband)?
428 } else {
429 raw
430 };
431
432 apply_report_status(&report, &mut plan.decisions);
433
434 let results: Vec<_> = plan.decisions.into_iter().map(|d| d.result).collect();
435 sideband.finish_pending();
436 let remote_messages = sideband.remote_messages;
437 net_trace!(
438 opts.network_trace,
439 opts.diagnostics.as_ref(),
440 "push_remote: done — {} result(s)",
441 results.len()
442 );
443 finish_push_outcome(local, opts, results, remote_messages)
444}
445
446pub fn push_http(
477 client: std::sync::Arc<dyn crate::transport::http::HttpClient>,
478 local_git_dir: &Path,
479 repo_url: &str,
480 refs: &[PushRefSpec],
481 opts: &PushOptions,
482 progress: &mut dyn Progress,
483 cancel: &dyn Cancel,
484) -> Result<PushOutcome> {
485 use crate::transport::http::SmartHttpTransport;
486 use crate::transport::{ConnectOptions, Service, Transport};
487 let connect = ConnectOptions {
488 protocol_version: 0,
489 diagnostics: opts.diagnostics.clone(),
490 network_trace: opts.network_trace,
491 ..Default::default()
492 };
493 let transport = SmartHttpTransport::new(client);
494 let mut conn = transport.connect(repo_url, Service::ReceivePack, &connect)?;
495 push_remote(local_git_dir, &mut *conn, refs, opts, progress, cancel)
496}
497
498struct AdvertisedState {
503 remote_refs: HashMap<String, ObjectId>,
505 advertised_haves: Vec<ObjectId>,
507 server_sideband: bool,
509 server_ofs_delta: bool,
511 server_push_options: bool,
513 server_atomic: bool,
515 push_bundle: Option<String>,
517}
518
519impl AdvertisedState {
520 fn from_connection(conn: &mut dyn Connection) -> Self {
524 let mut remote_refs: HashMap<String, ObjectId> = HashMap::new();
525 let mut advertised_haves: Vec<ObjectId> = Vec::new();
526 for (name, oid) in conn.advertised_refs() {
527 if name == ".have" {
528 advertised_haves.push(*oid);
529 } else {
530 remote_refs.insert(name.clone(), *oid);
531 }
532 }
533 let caps = conn.capabilities();
534 Self {
535 remote_refs,
536 advertised_haves,
537 server_sideband: caps
538 .iter()
539 .any(|c| c == "side-band-64k" || c == "side-band"),
540 server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
541 server_push_options: caps.iter().any(|c| c == "push-options"),
542 server_atomic: caps.iter().any(|c| c == "atomic"),
543 push_bundle: caps.iter().find_map(|c| {
544 c.strip_prefix(crate::serve::PUSH_BUNDLE_CAPABILITY)?
545 .strip_prefix('=')
546 .map(str::to_owned)
547 }),
548 }
549 }
550}
551
552struct PushPlan {
555 decisions: Vec<PushDecision>,
556 to_send: Vec<usize>,
558}
559
560enum PlanOutcome {
564 Send(PushPlan),
565 Done(Vec<PushRefResult>),
566}
567
568fn plan_push(
573 refs: &[PushRefSpec],
574 local: &Repository,
575 adv: &AdvertisedState,
576 opts: &PushOptions,
577 pre_push: Option<&PrePushGate<'_>>,
578) -> Result<PlanOutcome> {
579 let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
580 for spec in refs {
581 decisions.push(decide_push_wire(spec, local, &adv.remote_refs)?);
582 }
583 if let Some(gate) = pre_push {
584 gate.check(refs.iter().zip(decisions.iter().map(|d| &d.result)))?;
585 }
586
587 let any_rejected = decisions.iter().any(|d| d.result.status.is_error());
590 if opts.atomic && any_rejected {
591 for d in &mut decisions {
592 if matches!(d.result.status, PushRefStatus::Ok) {
593 d.result.status = PushRefStatus::AtomicPushFailed;
594 d.send = false;
595 }
596 }
597 return Ok(PlanOutcome::Done(
598 decisions.into_iter().map(|d| d.result).collect(),
599 ));
600 }
601
602 let to_send: Vec<usize> = decisions
603 .iter()
604 .enumerate()
605 .filter_map(|(i, d)| if d.send { Some(i) } else { None })
606 .collect();
607
608 if to_send.is_empty() || opts.dry_run {
610 return Ok(PlanOutcome::Done(
611 decisions.into_iter().map(|d| d.result).collect(),
612 ));
613 }
614
615 Ok(PlanOutcome::Send(PushPlan { decisions, to_send }))
616}
617
618fn require_push_capabilities(adv: &AdvertisedState, opts: &PushOptions) -> Result<()> {
621 if opts.atomic && !adv.server_atomic {
622 return Err(Error::PushAtomicUnsupported);
623 }
624 if !opts.push_options.is_empty() && !adv.server_push_options {
625 return Err(Error::PushOptionsUnsupported);
626 }
627 Ok(())
628}
629
630fn build_command_block(
644 plan: &PushPlan,
645 adv: &AdvertisedState,
646 algo: HashAlgo,
647 opts: &PushOptions,
648) -> Result<Vec<u8>> {
649 let zero_hex = "0".repeat(algo.hex_len());
650 let mut command_caps = PUSH_CAPS_BASE.to_owned();
651 if adv.server_sideband {
652 command_caps.push_str(" side-band-64k");
653 }
654 if !opts.push_options.is_empty() {
655 command_caps.push_str(" push-options");
656 }
657 if opts.atomic && adv.server_atomic {
658 command_caps.push_str(" atomic");
659 }
660 command_caps.push_str(&format!(" object-format={}", algo.name()));
661
662 let mut commands: Vec<u8> = Vec::new();
663 let mut first = true;
664 for &i in &plan.to_send {
665 let d = &plan.decisions[i];
666 let old_hex = d
667 .result
668 .old_oid
669 .map(|o| o.to_hex())
670 .unwrap_or_else(|| zero_hex.clone());
671 let new_hex = d
672 .result
673 .new_oid
674 .map(|o| o.to_hex())
675 .unwrap_or_else(|| zero_hex.clone());
676 let line = if first {
682 first = false;
683 format!(
684 "{old_hex} {new_hex} {}\0{command_caps}",
685 d.result.remote_ref
686 )
687 } else {
688 format!("{old_hex} {new_hex} {}", d.result.remote_ref)
689 };
690 pkt_line::write_line_to_vec(&mut commands, &line)?;
691 }
692 commands.extend_from_slice(b"0000");
696 if !opts.push_options.is_empty() {
697 for opt in &opts.push_options {
698 pkt_line::write_line_to_vec(&mut commands, opt)?;
699 }
700 commands.extend_from_slice(b"0000");
701 }
702 Ok(commands)
703}
704
705const PACK_SEND_BUFFER: usize = 64 * 1024;
708
709struct PushPack {
713 wants: Vec<ObjectId>,
714 haves: Vec<ObjectId>,
715 opts: PackBuildOptions,
716}
717
718impl PushPack {
719 fn for_plan(
728 plan: &PushPlan,
729 local: &Repository,
730 adv: &AdvertisedState,
731 ) -> Result<Option<Self>> {
732 let wants: Vec<ObjectId> = plan
733 .to_send
734 .iter()
735 .filter_map(|&i| plan.decisions[i].new_tip)
736 .collect();
737 if wants.is_empty() {
738 return Ok(None);
739 }
740 let haves = local.odb().present_oids(
741 adv.remote_refs
742 .values()
743 .chain(&adv.advertised_haves)
744 .copied(),
745 )?;
746 let local_cfg = crate::config::ConfigSet::load(
747 &crate::environment::Environment::empty(),
748 Some(local.git_dir()),
749 true,
750 )
751 .ok();
752 let mut opts = if haves.is_empty() {
753 PackBuildOptions::for_local_copy(local_cfg.as_ref())
754 } else {
755 PackBuildOptions::for_local_push(local_cfg.as_ref())
756 };
757 opts.use_ofs_delta = adv.server_ofs_delta;
758 Ok(Some(Self { wants, haves, opts }))
759 }
760
761 fn write_to(
763 &self,
764 local: &Repository,
765 w: &mut dyn Write,
766 stop: &crate::progress::CancelSignal<'_>,
767 ) -> Result<PackStats> {
768 crate::pack_objects::write_pack_for_push_with_cancel(
769 local.odb(),
770 &self.wants,
771 &self.haves,
772 &self.opts,
773 w,
774 stop,
775 )
776 }
777}
778
779struct PushDecision {
781 result: PushRefResult,
782 new_tip: Option<ObjectId>,
784 send: bool,
786}
787
788fn decide_push_wire(
793 spec: &PushRefSpec,
794 local: &Repository,
795 remote_refs: &HashMap<String, ObjectId>,
796) -> Result<PushDecision> {
797 let local_odb = local.odb();
798 let remote_current = remote_refs.get(&spec.dst).copied();
799
800 let no_op = |status: PushRefStatus,
801 old: Option<ObjectId>,
802 new: Option<ObjectId>,
803 deletion: bool,
804 message: Option<String>| {
805 PushDecision {
806 result: PushRefResult {
807 local_ref: crate::transfer::push_spec_local_ref(spec),
808 remote_ref: spec.dst.clone(),
809 old_oid: old,
810 new_oid: new,
811 forced: false,
812 deletion,
813 status,
814 message,
815 },
816 new_tip: None,
817 send: false,
818 }
819 };
820
821 if !spec.delete {
824 if let Some(src) = spec.src {
825 if remote_current == Some(src) {
826 return Ok(no_op(
827 PushRefStatus::UpToDate,
828 remote_current,
829 Some(src),
830 false,
831 None,
832 ));
833 }
834 }
835 }
836
837 if spec.expect_absent && remote_current.is_some() {
839 return Ok(no_op(
840 PushRefStatus::RejectStale,
841 remote_current,
842 spec.src,
843 spec.delete,
844 Some("stale info".to_owned()),
845 ));
846 }
847
848 if let Some(expected) = spec.expected_old {
850 if remote_current != Some(expected) {
851 return Ok(no_op(
852 PushRefStatus::RejectStale,
853 remote_current,
854 spec.src,
855 spec.delete,
856 Some("stale info".to_owned()),
857 ));
858 }
859 }
860
861 if spec.delete {
862 return Ok(match remote_current {
863 Some(_) => PushDecision {
864 result: PushRefResult {
865 local_ref: crate::transfer::push_spec_local_ref(spec),
866 remote_ref: spec.dst.clone(),
867 old_oid: remote_current,
868 new_oid: None,
869 forced: false,
870 deletion: true,
871 status: PushRefStatus::Ok,
872 message: None,
873 },
874 new_tip: None,
875 send: true,
876 },
877 None => no_op(
878 PushRefStatus::RejectRemoteRefMissing,
879 None,
880 None,
881 true,
882 Some("remote ref does not exist".to_owned()),
883 ),
884 });
885 }
886
887 let Some(src) = spec.src else {
888 return Err(Error::Message(format!(
889 "push to '{}' has no source object and is not a deletion",
890 spec.dst
891 )));
892 };
893 if !local_odb.contains(&src)? {
894 return Err(Error::Message(format!(
895 "source object {src} for '{}' is missing from the local object store",
896 spec.dst
897 )));
898 }
899
900 let Some(old) = remote_current else {
902 return Ok(PushDecision {
903 result: PushRefResult {
904 local_ref: crate::transfer::push_spec_local_ref(spec),
905 remote_ref: spec.dst.clone(),
906 old_oid: None,
907 new_oid: Some(src),
908 forced: false,
909 deletion: false,
910 status: PushRefStatus::Ok,
911 message: None,
912 },
913 new_tip: Some(src),
914 send: true,
915 });
916 };
917
918 if !spec.force && !local_odb.contains(&old)? {
919 return Ok(no_op(
920 PushRefStatus::RejectFetchFirst,
921 Some(old),
922 Some(src),
923 false,
924 Some("fetch first".to_owned()),
925 ));
926 }
927
928 let is_ff = if spec.force {
931 false
932 } else if crate::merge_base::is_ancestor_odb_walk_with_graph(
933 local_odb,
934 local_odb.commit_graph_chain().as_deref(),
935 old,
936 src,
937 )? {
938 true
939 } else {
940 crate::merge_base::is_ancestor(local, old, src).unwrap_or(false)
941 };
942
943 if is_ff {
944 Ok(PushDecision {
945 result: PushRefResult {
946 local_ref: crate::transfer::push_spec_local_ref(spec),
947 remote_ref: spec.dst.clone(),
948 old_oid: Some(old),
949 new_oid: Some(src),
950 forced: false,
951 deletion: false,
952 status: PushRefStatus::Ok,
953 message: None,
954 },
955 new_tip: Some(src),
956 send: true,
957 })
958 } else if spec.force {
959 Ok(PushDecision {
960 result: PushRefResult {
961 local_ref: crate::transfer::push_spec_local_ref(spec),
962 remote_ref: spec.dst.clone(),
963 old_oid: Some(old),
964 new_oid: Some(src),
965 forced: true,
966 deletion: false,
967 status: PushRefStatus::Ok,
968 message: None,
969 },
970 new_tip: Some(src),
971 send: true,
972 })
973 } else {
974 Ok(PushDecision {
975 result: PushRefResult {
976 local_ref: crate::transfer::push_spec_local_ref(spec),
977 remote_ref: spec.dst.clone(),
978 old_oid: Some(old),
979 new_oid: Some(src),
980 forced: false,
981 deletion: false,
982 status: PushRefStatus::RejectNonFastForward,
983 message: Some("non-fast-forward".to_owned()),
984 },
985 new_tip: None,
986 send: false,
987 })
988 }
989}
990
991pub(crate) fn read_receive_pack_response(
1000 reader: &mut dyn Read,
1001 sent_refs: &[&str],
1002 server_sideband: bool,
1003 cancel: &dyn Cancel,
1004) -> Result<Vec<u8>> {
1005 let mut raw = Vec::new();
1006 let mut buf = [0u8; 8192];
1007 loop {
1008 if cancel.is_cancelled() {
1009 if receive_pack_response_complete(&raw, sent_refs, server_sideband)? {
1010 break;
1011 }
1012 return Err(Error::Cancelled);
1013 }
1014 match reader.read(&mut buf) {
1015 Ok(0) => break,
1016 Ok(n) => raw.extend_from_slice(&buf[..n]),
1017 Err(e) if receive_pack_read_recoverable(&e) => {
1018 if receive_pack_response_complete(&raw, sent_refs, server_sideband)? {
1019 break;
1020 }
1021 return Err(Error::Io(e));
1022 }
1023 Err(e) => return Err(Error::Io(e)),
1024 }
1025 }
1026 if !receive_pack_response_complete(&raw, sent_refs, server_sideband)? {
1027 return Err(Error::Message(
1028 "receive-pack response ended before a complete report-status".to_owned(),
1029 ));
1030 }
1031 Ok(raw)
1032}
1033
1034fn receive_pack_read_recoverable(e: &std::io::Error) -> bool {
1035 matches!(
1036 e.kind(),
1037 ErrorKind::ConnectionReset | ErrorKind::BrokenPipe | ErrorKind::UnexpectedEof
1038 )
1039}
1040
1041fn receive_pack_response_complete(
1044 raw: &[u8],
1045 sent_refs: &[&str],
1046 server_sideband: bool,
1047) -> Result<bool> {
1048 let report = if server_sideband {
1049 demux_sideband_report(raw, None)?
1050 } else {
1051 raw.to_vec()
1052 };
1053 Ok(report_lines_complete(&report, sent_refs))
1054}
1055
1056fn report_lines_complete(report: &[u8], sent_refs: &[&str]) -> bool {
1059 let mut saw_unpack = false;
1060 let mut per_ref = HashSet::<String>::new();
1061 let mut cursor = Cursor::new(report);
1062 while let Ok(Some(pkt)) = pkt_line::read_packet(&mut cursor) {
1063 let Packet::Data(line) = pkt else {
1064 continue;
1065 };
1066 let line = line.trim_end();
1067 if line.starts_with("unpack ") {
1068 saw_unpack = true;
1069 } else if let Some(refname) = line.strip_prefix("ok ") {
1070 per_ref.insert(refname.trim().to_owned());
1071 } else if let Some(rest) = line.strip_prefix("ng ") {
1072 let (refname, _) = rest.split_once(' ').unwrap_or((rest, ""));
1073 per_ref.insert(refname.trim().to_owned());
1074 }
1075 }
1076 saw_unpack && sent_refs.iter().all(|r| per_ref.contains(*r))
1077}
1078
1079fn apply_report_status(report: &[u8], decisions: &mut [PushDecision]) {
1093 let mut by_ref: HashMap<&str, usize> = HashMap::new();
1094 for (i, d) in decisions.iter().enumerate() {
1095 if d.send {
1096 by_ref.insert(d.result.remote_ref.as_str(), i);
1097 }
1098 }
1099 let mut unpack_error: Option<String> = None;
1101 let mut updates: Vec<(usize, Option<String>)> = Vec::new();
1102
1103 let mut cursor = Cursor::new(report);
1104 while let Ok(Some(pkt)) = pkt_line::read_packet(&mut cursor) {
1105 let Packet::Data(line) = pkt else {
1106 continue;
1107 };
1108 let line = line.trim_end();
1109 if let Some(rest) = line.strip_prefix("unpack ") {
1110 if rest.trim() != "ok" {
1111 unpack_error = Some(rest.trim().to_owned());
1112 }
1113 } else if let Some(refname) = line.strip_prefix("ok ") {
1114 let _ = by_ref.get(refname.trim());
1116 } else if let Some(rest) = line.strip_prefix("ng ") {
1117 let (refname, reason) = rest.split_once(' ').unwrap_or((rest, ""));
1119 if let Some(&idx) = by_ref.get(refname.trim()) {
1120 let msg = if reason.trim().is_empty() {
1121 None
1122 } else {
1123 Some(reason.trim().to_owned())
1124 };
1125 updates.push((idx, msg));
1126 }
1127 }
1128 }
1129
1130 for (idx, msg) in updates {
1131 decisions[idx].result.status = PushRefStatus::RemoteRejected;
1132 decisions[idx].result.message = msg;
1133 }
1134
1135 if let Some(reason) = unpack_error {
1138 for d in decisions.iter_mut() {
1139 if d.send && !matches!(d.result.status, PushRefStatus::RemoteRejected) {
1140 d.result.status = PushRefStatus::RemoteRejected;
1141 d.result.message = Some(format!("unpack failed: {reason}"));
1142 }
1143 }
1144 }
1145}
1146
1147fn demux_sideband_report(input: &[u8], mut progress: Option<&mut dyn Progress>) -> Result<Vec<u8>> {
1152 let mut report = Vec::new();
1153 let mut i = 0usize;
1154 while i + 4 <= input.len() {
1155 let len = match pkt_line::parse_hex_len(&input[i..i + 4]) {
1156 Ok(l) => l,
1157 Err(_) => break,
1158 };
1159 i += 4;
1160 if len == 0 {
1161 continue;
1163 }
1164 if len < 4 || i + (len - 4) > input.len() {
1165 break;
1166 }
1167 let payload = &input[i..i + (len - 4)];
1168 i += len - 4;
1169 if payload.is_empty() {
1170 continue;
1171 }
1172 let band = payload[0];
1173 let data = &payload[1..];
1174 match band {
1175 1 => report.extend_from_slice(data),
1176 2 | 3 => {
1177 if let Some(p) = progress.as_deref_mut() {
1178 p.message(data);
1179 }
1180 }
1181 _ => {}
1182 }
1183 }
1184 Ok(report)
1185}
1186
1187fn demux_report_and_remote_messages(input: &[u8], progress: &mut dyn Progress) -> Result<Vec<u8>> {
1192 demux_sideband_report(input, Some(progress))
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197 use super::*;
1198 use crate::objects::ObjectKind;
1199
1200 fn make_decision(refname: &str, send: bool) -> PushDecision {
1201 PushDecision {
1202 result: PushRefResult {
1203 local_ref: None,
1204 remote_ref: refname.to_owned(),
1205 old_oid: None,
1206 new_oid: None,
1207 forced: false,
1208 deletion: false,
1209 status: PushRefStatus::Ok,
1210 message: None,
1211 },
1212 new_tip: None,
1213 send,
1214 }
1215 }
1216
1217 fn report_bytes(lines: &[&str]) -> Vec<u8> {
1218 let mut buf = Vec::new();
1219 for l in lines {
1220 pkt_line::write_line_to_vec(&mut buf, l).unwrap();
1221 }
1222 buf.extend_from_slice(b"0000");
1223 buf
1224 }
1225
1226 fn adv_state(sideband: bool, ofs_delta: bool, push_options: bool) -> AdvertisedState {
1227 AdvertisedState {
1228 remote_refs: HashMap::new(),
1229 advertised_haves: Vec::new(),
1230 server_sideband: sideband,
1231 server_ofs_delta: ofs_delta,
1232 server_push_options: push_options,
1233 server_atomic: false,
1234 push_bundle: None,
1235 }
1236 }
1237
1238 #[derive(Default)]
1239 struct TestConnection {
1240 caps: Vec<String>,
1241 refs: Vec<(String, ObjectId)>,
1242 input: Cursor<Vec<u8>>,
1243 output: Vec<u8>,
1244 stateless: bool,
1245 }
1246
1247 impl Connection for TestConnection {
1248 fn reader(&mut self) -> &mut dyn Read {
1249 &mut self.input
1250 }
1251 fn writer(&mut self) -> &mut dyn std::io::Write {
1252 &mut self.output
1253 }
1254 fn advertised_refs(&self) -> &[(String, ObjectId)] {
1255 &self.refs
1256 }
1257 fn capabilities(&self) -> &[String] {
1258 &self.caps
1259 }
1260 fn head_symref(&self) -> Option<&str> {
1261 None
1262 }
1263 fn protocol_version(&self) -> u8 {
1264 0
1265 }
1266 fn stateless_rpc(&self) -> bool {
1267 self.stateless
1268 }
1269 }
1270
1271 #[test]
1272 fn advertisement_recognizes_atomic_capability_token() {
1273 for caps in [
1274 vec![],
1275 vec!["atomic"],
1276 vec!["atomic=false"],
1277 vec!["atomic", "side-band-64k", "ofs-delta", "push-options"],
1278 ] {
1279 let mut conn = TestConnection {
1280 caps: caps.iter().map(|s| (*s).to_owned()).collect(),
1281 ..Default::default()
1282 };
1283 let adv = AdvertisedState::from_connection(&mut conn);
1284 assert_eq!(adv.server_atomic, caps.contains(&"atomic"));
1285 assert_eq!(adv.server_sideband, caps.contains(&"side-band-64k"));
1286 assert_eq!(adv.server_ofs_delta, caps.contains(&"ofs-delta"));
1287 assert_eq!(adv.server_push_options, caps.contains(&"push-options"));
1288 }
1289 }
1290
1291 #[test]
1292 fn command_block_requests_atomic_only_on_first_command_when_supported_and_requested() {
1293 let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1294 let plan = PushPlan {
1295 decisions: vec![
1296 send_decision("refs/heads/one", new),
1297 send_decision("refs/heads/two", new),
1298 ],
1299 to_send: vec![0, 1],
1300 };
1301 for supported in [false, true] {
1302 for requested in [false, true] {
1303 let mut adv = adv_state(true, true, true);
1304 adv.server_atomic = supported;
1305 let opts = PushOptions {
1306 atomic: requested,
1307 push_options: vec!["ci.skip".to_owned()],
1308 ..Default::default()
1309 };
1310 let block = build_command_block(&plan, &adv, HashAlgo::Sha1, &opts).unwrap();
1311 let pkts = decode_block(&block);
1312 let (command, caps) = pkts[0].as_deref().unwrap().split_once('\0').unwrap();
1313 assert_eq!(
1314 command,
1315 format!("{} {} refs/heads/one", "0".repeat(40), new)
1316 );
1317 let caps: Vec<_> = caps.split_whitespace().collect();
1318 assert_eq!(caps.contains(&"atomic"), supported && requested);
1319 assert!(caps.contains(&"push-options"));
1320 assert!(caps.contains(&"side-band-64k"));
1321 assert!(caps.contains(&"object-format=sha1"));
1322 assert_eq!(
1323 pkts[1].as_deref().unwrap(),
1324 format!("{} {} refs/heads/two", "0".repeat(40), new)
1325 );
1326 assert_eq!(&pkts[2..], &[None, Some("ci.skip".to_owned()), None]);
1327 }
1328 }
1329 }
1330
1331 #[test]
1332 fn unsupported_atomic_push_fails_before_any_update_or_pack_bytes() {
1333 let dir = tempfile::tempdir().unwrap();
1334 let repo = Repository::init(dir.path(), &crate::repo::InitOptions::default()).unwrap();
1335 let refs = [PushRefSpec {
1336 src: Some(repo.odb().write(ObjectKind::Blob, b"payload").unwrap()),
1337 dst: "refs/tags/payload".to_owned(),
1338 ..Default::default()
1339 }];
1340 for stateless in [false, true] {
1341 for dry_run in [false, true] {
1342 for refs in [&[][..], &refs[..]] {
1344 let mut conn = TestConnection {
1345 stateless,
1346 ..Default::default()
1347 };
1348 let opts = PushOptions {
1349 atomic: true,
1350 dry_run,
1351 ..Default::default()
1352 };
1353 let err = push_remote(
1354 repo.git_dir(),
1355 &mut conn,
1356 refs,
1357 &opts,
1358 &mut crate::fetch::NoProgress,
1359 &crate::progress::NeverCancel,
1360 )
1361 .unwrap_err();
1362 assert!(matches!(err, Error::PushAtomicUnsupported), "{err:?}");
1363 assert!(
1364 conn.output.is_empty(),
1365 "unsupported atomic push wrote bytes"
1366 );
1367 let mut adv = adv_state(false, false, false);
1368 adv.server_atomic = true;
1369 require_push_capabilities(&adv, &opts).unwrap();
1370 }
1371 }
1372 }
1373 }
1374
1375 fn decode_block(block: &[u8]) -> Vec<Option<String>> {
1379 let mut cur = Cursor::new(block);
1380 let mut out = Vec::new();
1381 while let Ok(pkt) = pkt_line::read_packet(&mut cur) {
1382 match pkt {
1383 Some(Packet::Data(s)) => out.push(Some(s.trim_end_matches('\n').to_owned())),
1384 Some(Packet::Flush) => out.push(None),
1385 _ => break,
1386 }
1387 }
1388 out
1389 }
1390
1391 fn push_pack_bytes(
1392 plan: &PushPlan,
1393 repo: &Repository,
1394 adv: &AdvertisedState,
1395 ) -> Result<Option<Vec<u8>>> {
1396 PushPack::for_plan(plan, repo, adv)?
1397 .map(|pack| {
1398 let mut bytes = Vec::new();
1399 pack.write_to(repo, &mut bytes, &crate::progress::CancelSignal::never())?;
1400 Ok(bytes)
1401 })
1402 .transpose()
1403 }
1404
1405 fn flaky_push_repo(
1406 dir: &Path,
1407 ) -> (
1408 Repository,
1409 std::sync::Arc<crate::odb::store::flaky::FlakyStore>,
1410 ) {
1411 let flaky = crate::odb::store::flaky::FlakyStore::new();
1412 let git_dir = dir.join("repo.git");
1413 let repo = crate::repo::RepositoryBuilder::new(RepositoryOptions::empty())
1414 .odb_builder(
1415 crate::odb::OdbBuilder::files(git_dir.join("objects"))
1416 .primary(flaky.clone())
1417 .alternates(false),
1418 )
1419 .ref_store(std::sync::Arc::new(
1420 crate::refs::store::MemoryRefStore::new(),
1421 ))
1422 .open(&git_dir, None)
1423 .unwrap();
1424 (repo, flaky)
1425 }
1426
1427 #[test]
1428 fn push_pack_haves_fail_on_backend_error_instead_of_being_dropped() {
1429 let dir = tempfile::tempdir().unwrap();
1430 let (repo, flaky) = flaky_push_repo(dir.path());
1431 let odb = repo.odb();
1432 let blob = odb.write(ObjectKind::Blob, b"to send").unwrap();
1433 let remote_tip = odb.write(ObjectKind::Blob, b"remote has this").unwrap();
1434 let plan = PushPlan {
1435 decisions: vec![send_decision("refs/heads/main", blob)],
1436 to_send: vec![0],
1437 };
1438 let mut adv = adv_state(false, true, false);
1439 adv.remote_refs
1440 .insert("refs/heads/main".to_owned(), remote_tip);
1441 assert!(push_pack_bytes(&plan, &repo, &adv).unwrap().is_some());
1442
1443 flaky.fail_contains(true);
1444 let err = push_pack_bytes(&plan, &repo, &adv).unwrap_err();
1445 assert!(crate::odb::store::flaky::is_injected(&err), "{err:?}");
1446 }
1447
1448 #[test]
1449 fn push_source_check_fails_on_backend_error_instead_of_missing() {
1450 let dir = tempfile::tempdir().unwrap();
1451 let (repo, flaky) = flaky_push_repo(dir.path());
1452 let src = repo.odb().write(ObjectKind::Blob, b"source").unwrap();
1453 let spec = PushRefSpec {
1454 src: Some(src),
1455 src_ref: None,
1456 dst: "refs/heads/main".to_owned(),
1457 force: false,
1458 delete: false,
1459 expected_old: None,
1460 expect_absent: false,
1461 hook_src_ref: None,
1462 };
1463 let decision = decide_push_wire(&spec, &repo, &HashMap::new()).unwrap();
1464 assert_eq!(decision.new_tip, Some(src));
1465
1466 flaky.fail_contains(true);
1467 let err = decide_push_wire(&spec, &repo, &HashMap::new())
1468 .err()
1469 .unwrap();
1470 assert!(crate::odb::store::flaky::is_injected(&err), "{err:?}");
1471 }
1472
1473 fn send_decision(refname: &str, new_oid: ObjectId) -> PushDecision {
1474 PushDecision {
1475 result: PushRefResult {
1476 local_ref: None,
1477 remote_ref: refname.to_owned(),
1478 old_oid: None,
1479 new_oid: Some(new_oid),
1480 forced: false,
1481 deletion: false,
1482 status: PushRefStatus::Ok,
1483 message: None,
1484 },
1485 new_tip: Some(new_oid),
1486 send: true,
1487 }
1488 }
1489
1490 #[test]
1491 fn command_block_without_push_options_has_no_capability_or_lines() {
1492 let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1493 let plan = PushPlan {
1494 decisions: vec![send_decision("refs/heads/main", new)],
1495 to_send: vec![0],
1496 };
1497 let block = build_command_block(
1498 &plan,
1499 &adv_state(false, false, true),
1500 HashAlgo::Sha1,
1501 &PushOptions::default(),
1502 )
1503 .unwrap();
1504 let pkts = decode_block(&block);
1505 assert_eq!(pkts.len(), 2);
1507 let cmd = pkts[0].as_deref().unwrap();
1508 assert!(
1509 cmd.contains("refs/heads/main"),
1510 "first line is the ref command, got {cmd:?}"
1511 );
1512 assert!(
1513 !cmd.contains("push-options"),
1514 "no push-options capability without options, got {cmd:?}"
1515 );
1516 assert_eq!(pkts[1], None, "single trailing flush");
1517 }
1518
1519 #[test]
1520 fn command_block_with_push_options_negotiates_cap_and_emits_lines() {
1521 let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1522 let plan = PushPlan {
1523 decisions: vec![send_decision("refs/heads/main", new)],
1524 to_send: vec![0],
1525 };
1526 let opts = PushOptions {
1527 push_options: vec!["ci.skip".to_owned(), "reviewer=alice".to_owned()],
1528 ..Default::default()
1529 };
1530 let block = build_command_block(&plan, &adv_state(true, true, true), HashAlgo::Sha1, &opts)
1531 .unwrap();
1532 let pkts = decode_block(&block);
1533 assert_eq!(
1535 pkts,
1536 vec![
1537 pkts[0].clone(),
1538 None,
1539 Some("ci.skip".to_owned()),
1540 Some("reviewer=alice".to_owned()),
1541 None,
1542 ],
1543 "push-option lines must follow the command-list flush, then a flush"
1544 );
1545 let cmd = pkts[0].as_deref().unwrap();
1546 assert!(
1547 cmd.contains("push-options"),
1548 "capability list must advertise push-options, got {cmd:?}"
1549 );
1550 assert!(cmd.contains("report-status"));
1552 assert!(cmd.contains("side-band-64k"));
1553 assert!(cmd.contains("object-format=sha1"));
1554 }
1555
1556 #[test]
1557 fn require_push_options_errors_typed_when_server_lacks_capability() {
1558 let opts = PushOptions {
1559 push_options: vec!["x".to_owned()],
1560 ..PushOptions::default()
1561 };
1562 let err = require_push_capabilities(&adv_state(true, true, false), &opts).unwrap_err();
1564 assert!(
1565 matches!(err, Error::PushOptionsUnsupported),
1566 "expected PushOptionsUnsupported, got {err:?}"
1567 );
1568 assert_eq!(
1569 err.to_string(),
1570 "the receiving end does not support push options"
1571 );
1572 require_push_capabilities(&adv_state(true, true, true), &opts).unwrap();
1574 require_push_capabilities(&adv_state(true, true, false), &PushOptions::default()).unwrap();
1576 }
1577
1578 #[test]
1579 fn report_ng_demotes_to_remote_rejected() {
1580 let mut decisions = vec![
1581 make_decision("refs/heads/main", true),
1582 make_decision("refs/heads/topic", true),
1583 ];
1584 let report = report_bytes(&[
1585 "unpack ok",
1586 "ok refs/heads/main",
1587 "ng refs/heads/topic non-fast-forward",
1588 ]);
1589 apply_report_status(&report, &mut decisions);
1590 assert_eq!(decisions[0].result.status, PushRefStatus::Ok);
1591 assert_eq!(decisions[1].result.status, PushRefStatus::RemoteRejected);
1592 assert_eq!(
1593 decisions[1].result.message.as_deref(),
1594 Some("non-fast-forward")
1595 );
1596 }
1597
1598 #[test]
1599 fn report_unpack_failure_rejects_all_sent() {
1600 let mut decisions = vec![make_decision("refs/heads/main", true)];
1601 let report = report_bytes(&["unpack index-pack abort"]);
1602 apply_report_status(&report, &mut decisions);
1603 assert_eq!(decisions[0].result.status, PushRefStatus::RemoteRejected);
1604 assert!(decisions[0]
1605 .result
1606 .message
1607 .as_deref()
1608 .unwrap()
1609 .starts_with("unpack failed:"));
1610 }
1611
1612 #[test]
1613 fn demux_separates_report_and_progress() {
1614 struct Cap(Vec<u8>);
1615 impl crate::progress::ProgressSink for Cap {}
1616 impl Progress for Cap {
1617 fn message(&mut self, bytes: &[u8]) {
1618 self.0.extend_from_slice(bytes);
1619 }
1620 }
1621 let mut wire = Vec::new();
1623 let mut band1 = vec![1u8];
1624 band1.extend_from_slice(b"unpack ok\n");
1625 pkt_line::write_packet_raw(&mut wire, &band1).unwrap();
1626 let mut band2 = vec![2u8];
1627 band2.extend_from_slice(b"hello from hook\n");
1628 pkt_line::write_packet_raw(&mut wire, &band2).unwrap();
1629 wire.extend_from_slice(b"0000");
1630
1631 let mut cap = Cap(Vec::new());
1632 let report = demux_report_and_remote_messages(&wire, &mut cap).unwrap();
1633 assert_eq!(report, b"unpack ok\n");
1634 assert_eq!(cap.0, b"hello from hook\n");
1635 }
1636
1637 #[test]
1638 fn report_lines_complete_requires_unpack_and_every_sent_ref() {
1639 let sent = ["refs/heads/main"];
1640 let full = report_bytes(&["unpack ok", "ok refs/heads/main"]);
1641 assert!(report_lines_complete(&full, &sent));
1642 let missing_ok = report_bytes(&["unpack ok"]);
1643 assert!(!report_lines_complete(&missing_ok, &sent));
1644 let missing_unpack = report_bytes(&["ok refs/heads/main"]);
1645 assert!(!report_lines_complete(&missing_unpack, &sent));
1646 let ng_ok = report_bytes(&["unpack ok", "ng refs/heads/main denied"]);
1647 assert!(report_lines_complete(&ng_ok, &sent));
1648 }
1649
1650 struct AbruptAfterRead {
1651 payload: Vec<u8>,
1652 pos: usize,
1653 err_kind: ErrorKind,
1654 }
1655
1656 impl Read for AbruptAfterRead {
1657 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1658 if self.pos < self.payload.len() {
1659 let n = std::cmp::min(buf.len(), self.payload.len() - self.pos);
1660 buf[..n].copy_from_slice(&self.payload[self.pos..self.pos + n]);
1661 self.pos += n;
1662 return Ok(n);
1663 }
1664 Err(std::io::Error::new(self.err_kind, "forcibly closed"))
1665 }
1666 }
1667
1668 fn sideband_receive_pack_wire(hook_line: &[u8]) -> Vec<u8> {
1669 let report_body = report_bytes(&["unpack ok", "ok refs/heads/main"]);
1670 let mut band1 = vec![1u8];
1671 band1.extend_from_slice(&report_body);
1672 let mut wire = Vec::new();
1673 pkt_line::write_packet_raw(&mut wire, &band1).unwrap();
1674 let mut band2 = vec![2u8];
1675 band2.extend_from_slice(hook_line);
1676 pkt_line::write_packet_raw(&mut wire, &band2).unwrap();
1677 wire.extend_from_slice(b"0000");
1678 wire
1679 }
1680
1681 struct EofAfterRead {
1682 payload: Vec<u8>,
1683 pos: usize,
1684 }
1685
1686 impl Read for EofAfterRead {
1687 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1688 if self.pos < self.payload.len() {
1689 let n = std::cmp::min(buf.len(), self.payload.len() - self.pos);
1690 buf[..n].copy_from_slice(&self.payload[self.pos..self.pos + n]);
1691 self.pos += n;
1692 Ok(n)
1693 } else {
1694 Ok(0)
1695 }
1696 }
1697 }
1698
1699 fn read_push_report(
1701 reader: &mut dyn Read,
1702 sent_refs: &[&str],
1703 sideband: bool,
1704 progress: &mut dyn Progress,
1705 ) -> Result<Vec<u8>> {
1706 let raw =
1707 read_receive_pack_response(reader, sent_refs, sideband, &crate::progress::NeverCancel)?;
1708 if sideband {
1709 demux_report_and_remote_messages(&raw, progress)
1710 } else {
1711 Ok(raw)
1712 }
1713 }
1714
1715 #[test]
1716 fn read_receive_pack_response_tolerates_reset_after_full_report() {
1717 let report = report_bytes(&["unpack ok", "ok refs/heads/main"]);
1718 let mut reader = AbruptAfterRead {
1719 payload: report.clone(),
1720 pos: 0,
1721 err_kind: ErrorKind::ConnectionReset,
1722 };
1723 let out = read_receive_pack_response(
1724 &mut reader,
1725 &["refs/heads/main"],
1726 false,
1727 &crate::progress::NeverCancel,
1728 )
1729 .expect("reset");
1730 assert_eq!(out, report);
1731 }
1732
1733 #[test]
1734 fn a_cancel_stops_an_incomplete_report_but_keeps_a_complete_one() {
1735 struct CancelAfterRead {
1737 payload: Cursor<Vec<u8>>,
1738 cancel: crate::progress::FlagCancel,
1739 }
1740 impl Read for CancelAfterRead {
1741 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1742 let n = self.payload.read(buf)?;
1743 self.cancel.cancel();
1744 Ok(n)
1745 }
1746 }
1747 let refs = ["refs/heads/main"];
1748 for (lines, complete) in [
1749 (&["unpack ok"][..], false),
1750 (&["unpack ok", "ok refs/heads/main"][..], true),
1751 ] {
1752 let cancel = crate::progress::FlagCancel::new();
1753 let report = report_bytes(lines);
1754 let mut reader = CancelAfterRead {
1755 payload: Cursor::new(report.clone()),
1756 cancel: cancel.clone(),
1757 };
1758 let out = read_receive_pack_response(&mut reader, &refs, false, &cancel);
1759 if complete {
1760 assert_eq!(out.expect("complete report"), report);
1761 } else {
1762 assert!(matches!(out, Err(Error::Cancelled)), "{out:?}");
1763 }
1764 }
1765 }
1766
1767 #[test]
1768 fn read_receive_pack_response_fails_reset_on_incomplete_report() {
1769 let partial = report_bytes(&["unpack ok"]);
1770 let mut reader = AbruptAfterRead {
1771 payload: partial,
1772 pos: 0,
1773 err_kind: ErrorKind::ConnectionReset,
1774 };
1775 let err = read_receive_pack_response(
1776 &mut reader,
1777 &["refs/heads/main"],
1778 false,
1779 &crate::progress::NeverCancel,
1780 )
1781 .unwrap_err();
1782 assert!(
1783 matches!(err, Error::Io(_)),
1784 "incomplete report must stay an I/O error, got {err:?}"
1785 );
1786 }
1787
1788 #[test]
1789 fn sideband_remote_messages_flush_unterminated_hook_text() {
1790 struct Noop;
1791 impl crate::progress::ProgressSink for Noop {}
1792 impl Progress for Noop {}
1793 let mut inner = Noop;
1794 let mut sideband = PushSidebandCollector {
1795 inner: &mut inner,
1796 remote_messages: Vec::new(),
1797 pending: SidebandLines::new(),
1798 };
1799 let hook = b"policy: no ticket";
1800 let wire = sideband_receive_pack_wire(hook);
1801 demux_report_and_remote_messages(&wire, &mut sideband).unwrap();
1802 sideband.finish_pending();
1803 assert_eq!(
1804 sideband.remote_messages,
1805 vec!["policy: no ticket".to_owned()]
1806 );
1807 }
1808
1809 #[test]
1810 fn sideband_diagnostics_delivered_once_on_clean_eof() {
1811 struct Cap(Vec<u8>);
1812 impl crate::progress::ProgressSink for Cap {}
1813 impl Progress for Cap {
1814 fn message(&mut self, bytes: &[u8]) {
1815 self.0.extend_from_slice(bytes);
1816 }
1817 }
1818 let hook = b"hook output\n";
1819 let wire = sideband_receive_pack_wire(hook);
1820 let mut reader = EofAfterRead {
1821 payload: wire,
1822 pos: 0,
1823 };
1824 let mut cap = Cap(Vec::new());
1825 read_push_report(&mut reader, &["refs/heads/main"], true, &mut cap)
1826 .expect("clean eof side-band push report");
1827 assert_eq!(cap.0.as_slice(), hook);
1828 }
1829
1830 #[test]
1831 fn sideband_diagnostics_delivered_once_on_connection_reset() {
1832 struct Cap(Vec<u8>);
1833 impl crate::progress::ProgressSink for Cap {}
1834 impl Progress for Cap {
1835 fn message(&mut self, bytes: &[u8]) {
1836 self.0.extend_from_slice(bytes);
1837 }
1838 }
1839 let hook = b"hook output\n";
1840 let wire = sideband_receive_pack_wire(hook);
1841 let mut reader = AbruptAfterRead {
1842 payload: wire,
1843 pos: 0,
1844 err_kind: ErrorKind::ConnectionReset,
1845 };
1846 let mut cap = Cap(Vec::new());
1847 read_push_report(&mut reader, &["refs/heads/main"], true, &mut cap)
1848 .expect("reset after complete side-band report");
1849 assert_eq!(cap.0.as_slice(), hook);
1850 }
1851
1852 #[cfg_attr(
1855 not(feature = "full-tests"),
1856 ignore = "starts system git or is heavy; run with --features full-tests"
1857 )]
1858 #[test]
1859 fn push_pack_initial_branch_push_uses_deltas() {
1860 use std::process::Command;
1861
1862 fn pack_delta_count(bytes: &[u8], algo_len: usize) -> usize {
1863 fn read_type_size(b: &[u8]) -> (u8, usize, usize) {
1864 let type_code = (b[0] >> 4) & 7;
1865 let mut size = (b[0] & 0x0f) as usize;
1866 let mut shift = 4;
1867 let mut i = 1;
1868 while b[i - 1] & 0x80 != 0 {
1869 size |= ((b[i] & 0x7f) as usize) << shift;
1870 shift += 7;
1871 i += 1;
1872 }
1873 (type_code, size, i)
1874 }
1875 fn zlib_consume(b: &[u8]) -> usize {
1876 use std::io::Read as _;
1877 let mut dec = flate2::bufread::ZlibDecoder::new(b);
1878 let mut sink = Vec::new();
1879 dec.read_to_end(&mut sink).expect("zlib");
1880 dec.total_in() as usize
1881 }
1882 let nr = u32::from_be_bytes(bytes[8..12].try_into().unwrap()) as usize;
1883 let mut pos = 12usize;
1884 let mut deltas = 0usize;
1885 for _ in 0..nr {
1886 let (type_code, _, consumed) = read_type_size(&bytes[pos..]);
1887 pos += consumed;
1888 match type_code {
1889 6 | 7 => {
1890 deltas += 1;
1891 if type_code == 7 {
1892 pos += algo_len;
1893 } else {
1894 while bytes[pos] & 0x80 != 0 {
1895 pos += 1;
1896 }
1897 pos += 1;
1898 }
1899 }
1900 _ => {}
1901 }
1902 pos += zlib_consume(&bytes[pos..]);
1903 }
1904 deltas
1905 }
1906
1907 let tmp = tempfile::tempdir().unwrap();
1908 let dir = tmp.path();
1909 for i in 0..40 {
1910 if i == 0 {
1911 Command::new("git")
1912 .current_dir(dir)
1913 .args(["init", "-q", "-b", "main", "."])
1914 .env("GIT_CONFIG_GLOBAL", "/dev/null")
1915 .env("GIT_CONFIG_SYSTEM", "/dev/null")
1916 .status()
1917 .unwrap();
1918 }
1919 std::fs::write(dir.join(format!("f{i:03}.txt")), format!("content {i}\n")).unwrap();
1920 std::fs::write(dir.join("shared.txt"), format!("shared rev {i}\n")).unwrap();
1921 Command::new("git")
1922 .current_dir(dir)
1923 .args(["add", "-A"])
1924 .env("GIT_CONFIG_GLOBAL", "/dev/null")
1925 .env("GIT_CONFIG_SYSTEM", "/dev/null")
1926 .status()
1927 .unwrap();
1928 Command::new("git")
1929 .current_dir(dir)
1930 .args(["commit", "-q", "-m", &format!("commit {i}")])
1931 .env("GIT_AUTHOR_NAME", "T")
1932 .env("GIT_AUTHOR_EMAIL", "t@example.com")
1933 .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
1934 .env("GIT_COMMITTER_NAME", "T")
1935 .env("GIT_COMMITTER_EMAIL", "t@example.com")
1936 .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
1937 .env("GIT_CONFIG_GLOBAL", "/dev/null")
1938 .env("GIT_CONFIG_SYSTEM", "/dev/null")
1939 .status()
1940 .unwrap();
1941 }
1942 Command::new("git")
1943 .current_dir(dir)
1944 .args(["repack", "-adf"])
1945 .env("GIT_CONFIG_GLOBAL", "/dev/null")
1946 .env("GIT_CONFIG_SYSTEM", "/dev/null")
1947 .status()
1948 .unwrap();
1949 let git_dir = dir.join(".git");
1950 let repo = Repository::open(&git_dir, &RepositoryOptions::empty()).unwrap();
1951 let odb = repo.odb();
1952 let tip = crate::refs::resolve_ref(&git_dir, "HEAD").expect("head");
1953 let plan = PushPlan {
1954 decisions: vec![send_decision("refs/heads/main", tip)],
1955 to_send: vec![0],
1956 };
1957 let adv = adv_state(false, true, false);
1958 let pack = push_pack_bytes(&plan, &repo, &adv)
1959 .unwrap()
1960 .expect("pack bytes");
1961 let deltas = pack_delta_count(&pack, odb.hash_algo().len());
1962 assert!(
1963 deltas > 0,
1964 "initial push pack must contain delta objects (got {deltas})"
1965 );
1966 }
1967}