1use std::collections::{HashMap, HashSet};
4use std::io::{BufReader, BufWriter, Read, Write};
5
6use super::connectivity::{PushedObjects, Unconnected};
7
8use super::{
9 check_object_format, for_each_advertised_ref, parse_oid, read_packet, ref_update_policy,
10 write_flush, write_line, Packet, Peel, ProtocolVersion, Result, ServeError, ServeOptions,
11};
12use crate::config::ConfigSet;
13use crate::fsck_msg_policy::{ObjectFsckPolicy, SkipListError};
14use crate::fsck_standalone::FsckError;
15use crate::hooks::{run_hook_in_git_dir, HookResult};
16use crate::index_pack::{install_pack_path, IngestPackOptions};
17use crate::objects::pack_stream_scan::{PackStreamScanError, PackStreamScanner};
18use crate::objects::{ObjectId, ObjectKind};
19use crate::odb::Odb;
20use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
21use crate::progress::{Cancel, CancelSignal, NeverCancel};
22use crate::receive_pack::{max_input_size_from_config, should_use_unpack_objects};
23use crate::receive_quarantine::ReceiveQuarantine;
24use crate::repo::Repository;
25use crate::shallow::{add_shallow_boundaries, load_shallow_boundaries, PushShallowInfo};
26use crate::unpack_objects::{unpack_objects, UnpackOptions};
27
28const SPOOL_CHUNK: usize = 256 * 1024;
30
31const CAPABILITIES: &[&str] = &[
34 "report-status",
35 "report-status-v2",
36 "delete-refs",
37 "side-band-64k",
38 "quiet",
39 "atomic",
40 "ofs-delta",
41];
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ReceivePolicy {
46 pub deny_non_fast_forwards: bool,
49 pub deny_deletes: bool,
51 pub deny_current_branch: bool,
54 pub deny_delete_current: bool,
57 pub fsck_objects: Option<ObjectFsckPolicy>,
62 pub shallow_update: bool,
70}
71
72impl Default for ReceivePolicy {
73 fn default() -> Self {
74 Self {
75 deny_non_fast_forwards: false,
76 deny_deletes: false,
77 deny_current_branch: true,
78 deny_delete_current: true,
79 fsck_objects: None,
80 shallow_update: false,
81 }
82 }
83}
84
85impl ReceivePolicy {
86 #[must_use]
108 pub fn from_config(config: &ConfigSet) -> Self {
109 let flag = |key: &str| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(false);
110 let allows = |key: &str, update_instead_allows: bool| {
112 config.get(key).is_some_and(|v| {
113 let v = v.to_ascii_lowercase();
114 matches!(v.as_str(), "ignore" | "warn" | "false" | "no" | "off" | "0")
115 || (update_instead_allows && v == "updateinstead")
116 })
117 };
118 let explicit = |key: &str| config.get_bool(key).map(|r| r.unwrap_or(true));
119 let fsck = explicit("receive.fsckObjects")
120 .or_else(|| explicit("transfer.fsckObjects"))
121 .unwrap_or(false);
122 Self {
123 deny_non_fast_forwards: flag("receive.denyNonFastForwards"),
124 deny_deletes: flag("receive.denyDeletes"),
125 deny_current_branch: !allows("receive.denyCurrentBranch", true),
126 deny_delete_current: !allows("receive.denyDeleteCurrent", false),
127 fsck_objects: fsck.then(|| ObjectFsckPolicy::from_config(config, "receive.fsck")),
128 shallow_update: flag("receive.shallowUpdate"),
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct RejectedObject {
136 pub oid: ObjectId,
138 pub kind: ObjectKind,
140 pub error: FsckError,
142}
143
144impl std::fmt::Display for RejectedObject {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(
147 f,
148 "{} {} refused: {}",
149 self.kind,
150 self.oid.to_hex(),
151 self.error.report_line()
152 )
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct RefCommand {
159 pub refname: String,
161 pub old: Option<ObjectId>,
163 pub new: Option<ObjectId>,
165}
166
167impl RefCommand {
168 fn refused(&self, reason: RefUpdateRefusal) -> RefUpdateResult {
169 RefUpdateResult {
170 refname: self.refname.clone(),
171 old: self.old,
172 new: self.new,
173 error: Some(reason),
174 }
175 }
176
177 fn accepted(&self) -> RefUpdateResult {
178 RefUpdateResult {
179 refname: self.refname.clone(),
180 old: self.old,
181 new: self.new,
182 error: None,
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
193#[non_exhaustive]
194pub enum RefUpdateRefusal {
195 #[error("funny refname")]
197 InvalidRefName,
198 #[error("deny updating a hidden ref")]
200 HiddenRef,
201 #[error("stale info")]
203 StaleInfo,
204 #[error("deletion prohibited")]
206 DeletionProhibited,
207 #[error("deletion of the current branch prohibited")]
210 CurrentBranchDeletion,
211 #[error("branch is currently checked out")]
214 CurrentBranch,
215 #[error("missing necessary objects")]
217 MissingObjects,
218 #[error("missing necessary objects")]
225 Unconnected {
226 missing: ObjectId,
228 referrer: ObjectId,
230 },
231 #[error("non-fast-forward")]
233 NonFastForward,
234 #[error("bad ref")]
237 BadRef,
238 #[error("unable to verify fast-forward")]
242 FastForwardCheckFailed,
243 #[error("shallow update not allowed")]
247 ShallowUpdateNotAllowed,
248 #[error("unpacker error")]
251 UnpackerError,
252 #[error("atomic transaction failed")]
255 AtomicTransactionFailed,
256 #[error("pre-receive hook declined")]
258 PreReceiveHookDeclined,
259 #[error("hook declined")]
261 UpdateHookDeclined,
262 #[error("{0}")]
264 Policy(String),
265 #[error("{target} is the target branch; it changes only through integrate")]
269 TargetBranch {
270 target: String,
272 },
273 #[error("failed to update ref: {0}")]
275 RefStore(String),
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct RefUpdateResult {
281 pub refname: String,
283 pub old: Option<ObjectId>,
285 pub new: Option<ObjectId>,
287 pub error: Option<RefUpdateRefusal>,
289}
290
291#[derive(Debug, thiserror::Error)]
297#[non_exhaustive]
298pub enum UnpackError {
299 #[error("unexpected end of pack stream")]
301 Truncated,
302 #[error("pack exceeds maximum input size of {limit} bytes")]
304 TooLarge {
305 limit: u64,
307 },
308 #[error(transparent)]
310 Malformed(#[from] PackStreamScanError),
311 #[error(transparent)]
314 Ingest(Box<crate::error::Error>),
315 #[error("{0}")]
317 Rejected(RejectedObject),
318 #[error(transparent)]
321 FsckSkipList(#[from] SkipListError),
322 #[error(transparent)]
324 Io(#[from] std::io::Error),
325 #[error("cancelled")]
327 Cancelled,
328}
329
330impl From<crate::error::Error> for UnpackError {
331 fn from(err: crate::error::Error) -> Self {
332 Self::Ingest(Box::new(err))
333 }
334}
335
336pub trait ReceiveHook {
351 fn pre_receive(
367 &self,
368 commands: &[RefCommand],
369 odb: &Odb,
370 ) -> std::result::Result<(), RefUpdateRefusal> {
371 let _ = (commands, odb);
372 Ok(())
373 }
374
375 fn pre_receive_ctx(
377 &self,
378 commands: &[RefCommand],
379 odb: &Odb,
380 ctx: &ReceiveHookContext<'_>,
381 ) -> std::result::Result<(), RefUpdateRefusal> {
382 let _ = ctx;
383 self.pre_receive(commands, odb)
384 }
385
386 fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
401 let _ = (command, odb);
402 Ok(())
403 }
404
405 fn update_ctx(
407 &self,
408 command: &RefCommand,
409 odb: &Odb,
410 ctx: &ReceiveHookContext<'_>,
411 ) -> std::result::Result<(), RefUpdateRefusal> {
412 let _ = ctx;
413 self.update(command, odb)
414 }
415
416 fn message(&self, updates: &[RefUpdateResult]) -> Option<String> {
432 let _ = updates;
433 None
434 }
435
436 fn post_receive(
443 &self,
444 commands: &[&RefCommand],
445 odb: &Odb,
446 ctx: &ReceiveHookContext<'_>,
447 ) -> std::result::Result<(), RefUpdateRefusal> {
448 let _ = (commands, odb, ctx);
449 Ok(())
450 }
451
452 fn post_update(
457 &self,
458 commands: &[&RefCommand],
459 odb: &Odb,
460 ) -> std::result::Result<(), RefUpdateRefusal> {
461 let _ = (commands, odb);
462 Ok(())
463 }
464}
465
466#[derive(Debug, Clone, Copy, Default)]
468pub struct ReceiveHookContext<'a> {
469 pub push_options: &'a [String],
471}
472
473impl ReceiveHook for () {}
474
475impl<F> ReceiveHook for F
476where
477 F: Fn(&RefCommand, &Odb) -> std::result::Result<(), RefUpdateRefusal>,
478{
479 fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
480 self(command, odb)
481 }
482}
483
484#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct HookRunRecord {
487 pub name: String,
489 pub ran: bool,
491 pub exit_code: Option<i32>,
493}
494
495#[derive(Debug, Default)]
497pub struct ReceiveOutcome {
498 pub unpack_error: Option<UnpackError>,
500 pub updates: Vec<RefUpdateResult>,
502 pub hooks: Vec<HookRunRecord>,
504 pub push_options: Vec<String>,
506 pub bytes_received: u64,
508 pub objects_received: u32,
510 pub post_receive_callback_error: Option<RefUpdateRefusal>,
512 pub post_update_callback_error: Option<RefUpdateRefusal>,
514}
515
516struct AppliedPush {
517 updates: Vec<RefUpdateResult>,
518 post_receive_callback_error: Option<RefUpdateRefusal>,
519 post_update_callback_error: Option<RefUpdateRefusal>,
520}
521
522fn applied_push(updates: Vec<RefUpdateResult>) -> AppliedPush {
523 AppliedPush {
524 updates,
525 post_receive_callback_error: None,
526 post_update_callback_error: None,
527 }
528}
529
530pub fn receive_pack(
539 repo: &Repository,
540 input: &mut dyn Read,
541 output: &mut dyn Write,
542 opts: &ServeOptions,
543 policy: &ReceivePolicy,
544) -> Result<ReceiveOutcome> {
545 receive_pack_with_hook(repo, input, output, opts, policy, &())
546}
547
548pub fn receive_pack_with_hook(
602 repo: &Repository,
603 input: &mut dyn Read,
604 output: &mut dyn Write,
605 opts: &ServeOptions,
606 policy: &ReceivePolicy,
607 hook: &dyn ReceiveHook,
608) -> Result<ReceiveOutcome> {
609 receive_pack_with_hook_cancellable(repo, input, output, opts, policy, hook, &NeverCancel)
610}
611
612pub fn receive_pack_with_hook_cancellable(
622 repo: &Repository,
623 input: &mut dyn Read,
624 output: &mut dyn Write,
625 opts: &ServeOptions,
626 policy: &ReceivePolicy,
627 hook: &dyn ReceiveHook,
628 cancel: &dyn Cancel,
629) -> Result<ReceiveOutcome> {
630 let stop = CancelSignal::new(cancel);
631 stop.poll().map_err(|_| ServeError::Cancelled)?;
632 if opts.advertise_refs || !opts.stateless_rpc {
633 if opts.protocol == ProtocolVersion::V1 {
634 write_line(output, "version 1")?;
635 }
636 write_advertisement(repo, output, opts)?;
637 output.flush()?;
638 if opts.advertise_refs {
639 return Ok(ReceiveOutcome::default());
640 }
641 }
642
643 let Some(request) = read_commands(repo, input)? else {
644 return Ok(ReceiveOutcome::default());
645 };
646
647 let cfg = repo
648 .config()
649 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
650 let max_input = max_input_size_from_config(cfg.as_ref());
651 let sideband = request.caps.contains("side-band-64k");
652
653 let needs_pack = request.commands.iter().any(|c| c.new.is_some());
654 let mut bytes_received = 0u64;
655 let mut objects_received = 0u32;
656 let mut quarantine = if needs_pack {
657 Some(ReceiveQuarantine::create(repo.odb(), repo.git_dir())?)
658 } else {
659 None
660 };
661
662 let receiver_grafts = if request.shallow.is_empty() {
663 HashSet::new()
664 } else {
665 load_shallow_boundaries(repo.git_dir())
666 };
667 let mut shallow_info =
668 PushShallowInfo::classify(&request.shallow, repo.odb(), &receiver_grafts)?;
669 let client_boundaries: HashSet<ObjectId> = request.shallow.iter().copied().collect();
670
671 let promises = if crate::promisor::repo_treats_promisor_packs(repo.git_dir(), cfg.as_ref()) {
674 Some(super::promised_objects::PromisorObjects::new(repo)?)
675 } else {
676 None
677 };
678 let existing = if needs_pack {
680 existing_ref_values(repo)?
681 } else {
682 HashSet::new()
683 };
684 let mut unconnected: HashMap<ObjectId, Unconnected> = HashMap::new();
686 let unpack_error = match quarantine.as_ref() {
687 Some(q) => {
688 let odb = q.odb();
689 spool_push_pack(input, &odb, max_input, &stop)
690 .and_then(|pack| {
691 bytes_received = pack.bytes_received();
692 objects_received = pack.object_count();
693 ingest_spooled_pack(
694 &odb,
695 pack,
696 cfg.as_ref(),
697 &client_boundaries,
698 promises.as_ref(),
699 )
700 })
701 .and_then(|indexed| {
702 if let Some(fsck) = &policy.fsck_objects {
703 check_received_objects(repo, &odb, indexed.as_ref(), cfg.as_ref(), fsck)?;
704 }
705 let received = match indexed {
708 Some(received) => received,
709 None => odb
710 .enumerate_local_loose_objects()?
711 .into_iter()
712 .map(|(oid, _)| oid)
713 .collect(),
714 };
715 unconnected = unconnected_tips(
716 repo,
717 &odb,
718 &received,
719 &existing,
720 &request.commands,
721 &client_boundaries,
722 promises.as_ref(),
723 )?;
724 Ok(())
725 })
726 .err()
727 }
728 None => None,
729 };
730 if matches!(unpack_error, Some(UnpackError::Cancelled)) {
731 return Err(ServeError::Cancelled);
732 }
733
734 let shallow = match quarantine.as_ref() {
735 Some(q) if unpack_error.is_none() => {
736 shallow_info.retain_received(&q.odb())?;
737 ShallowPlan::new(
738 repo,
739 &q.odb(),
740 &request,
741 policy,
742 &shallow_info,
743 &receiver_grafts,
744 )?
745 }
746 _ => ShallowPlan::none(request.commands.len()),
747 };
748
749 let mut hook_records = Vec::new();
750 let mut post_receive_callback_error = None;
751 let mut post_update_callback_error = None;
752 let updates = if unpack_error.is_some() {
753 request
754 .commands
755 .iter()
756 .map(|c| c.refused(RefUpdateRefusal::UnpackerError))
757 .collect()
758 } else {
759 stop.poll().map_err(|_| ServeError::Cancelled)?;
760 let applied = apply_commands_with_hooks(
761 repo,
762 &request,
763 opts,
764 policy,
765 hook,
766 quarantine.as_mut(),
767 &unconnected,
768 &shallow,
769 sideband.then_some(&mut *output),
770 &mut hook_records,
771 )?;
772 post_receive_callback_error = applied.post_receive_callback_error;
773 post_update_callback_error = applied.post_update_callback_error;
774 applied.updates
775 };
776
777 if sideband {
778 if let Some(text) = hook.message(&updates) {
779 write_progress_text(output, &text)?;
780 }
781 }
782 if request.caps.contains("report-status") || request.caps.contains("report-status-v2") {
783 let report = build_report(unpack_error.as_ref(), &updates)?;
784 if sideband {
785 crate::pkt_line::write_sideband_channel1_64k(&mut &mut *output, &report)?;
786 write_flush(output)?;
787 } else {
788 output.write_all(&report)?;
789 }
790 }
791 output.flush()?;
792
793 Ok(ReceiveOutcome {
794 unpack_error,
795 updates,
796 hooks: hook_records,
797 push_options: request.push_options.clone(),
798 bytes_received,
799 objects_received,
800 post_receive_callback_error,
801 post_update_callback_error,
802 })
803}
804
805fn write_progress_text(output: &mut dyn Write, text: &str) -> Result<()> {
808 for line in text.lines() {
809 let line = format!("{line}\n");
810 for chunk in line.as_bytes().chunks(65515) {
811 crate::pkt_line::write_sideband_packet(&mut *output, 2, chunk)
812 .map_err(ServeError::Io)?;
813 }
814 }
815 Ok(())
816}
817
818fn write_advertisement(
820 repo: &Repository,
821 output: &mut dyn Write,
822 opts: &ServeOptions,
823) -> Result<()> {
824 let mut caps: Vec<String> = CAPABILITIES.iter().map(|c| (*c).to_owned()).collect();
825 let config = repo
826 .config()
827 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
828 if config
829 .get_bool("receive.advertisepushoptions")
830 .and_then(|r| r.ok())
831 .unwrap_or(false)
832 {
833 caps.push("push-options".to_owned());
834 }
835 caps.push(format!("object-format={}", repo.odb().hash_algo().name()));
836 if let Some(path) = &opts.push_bundle {
837 caps.push(format!("{}={path}", super::PUSH_BUNDLE_CAPABILITY));
838 }
839 if !opts.agent.is_empty() {
840 caps.push(format!("agent={}", opts.agent));
841 }
842 let caps = caps.join(" ");
843
844 let mut first_line = true;
845 for_each_advertised_ref(repo, &opts.hidden_refs, &[], Peel::No, |r| {
846 if first_line {
847 write_line(output, &format!("{} {}\0{caps}", r.oid.to_hex(), r.name))?;
848 first_line = false;
849 } else {
850 write_line(output, &format!("{} {}", r.oid.to_hex(), r.name))?;
851 }
852 Ok(std::ops::ControlFlow::Continue(()))
853 })?;
854 if first_line {
855 let zero = ObjectId::null(repo.odb().hash_algo());
856 write_line(
857 output,
858 &format!("{} capabilities^{{}}\0{caps}", zero.to_hex()),
859 )?;
860 }
861 write_flush(output)
862}
863
864struct PushRequest {
866 commands: Vec<RefCommand>,
867 caps: HashSet<String>,
868 push_options: Vec<String>,
869 shallow: Vec<ObjectId>,
871}
872
873fn read_commands(repo: &Repository, input: &mut dyn Read) -> Result<Option<PushRequest>> {
879 let mut commands = Vec::new();
880 let mut caps = HashSet::new();
881 let mut shallow = Vec::new();
882 loop {
883 match read_packet(input)? {
884 None | Some(Packet::Flush) => break,
885 Some(Packet::Data(line)) => {
886 if let Some(hex) = line.strip_prefix("shallow ") {
887 shallow.push(parse_oid(repo, hex)?);
888 continue;
889 }
890 let (line, line_caps) = line.split_once('\0').unwrap_or((line.as_str(), ""));
891 if commands.is_empty() {
892 caps.extend(
893 line_caps
894 .split(' ')
895 .filter(|c| !c.is_empty())
896 .map(str::to_owned),
897 );
898 }
899 let mut parts = line.splitn(3, ' ');
900 let (Some(old), Some(new), Some(refname)) =
901 (parts.next(), parts.next(), parts.next())
902 else {
903 return Err(ServeError::Protocol(format!("malformed command '{line}'")));
904 };
905 let old = parse_oid(repo, old)?;
906 let new = parse_oid(repo, new)?;
907 commands.push(RefCommand {
908 old: (!old.is_zero()).then_some(old),
909 new: (!new.is_zero()).then_some(new),
910 refname: refname.to_owned(),
911 });
912 }
913 Some(other) => {
914 return Err(ServeError::Protocol(format!(
915 "unexpected packet in command list: {other:?}"
916 )))
917 }
918 }
919 }
920 if commands.is_empty() {
921 return Ok(None);
922 }
923 let mut push_options = Vec::new();
924 if caps.contains("push-options") {
925 loop {
926 match read_packet(input)? {
927 None | Some(Packet::Flush) => break,
928 Some(Packet::Data(line)) => {
929 push_options.push(line);
930 }
931 Some(other) => {
932 return Err(ServeError::Protocol(format!(
933 "unexpected packet in push options: {other:?}"
934 )))
935 }
936 }
937 }
938 }
939 let format = caps.iter().find_map(|c| c.strip_prefix("object-format="));
940 check_object_format(repo, format)?;
941 Ok(Some(PushRequest {
942 commands,
943 caps,
944 push_options,
945 shallow,
946 }))
947}
948
949struct ShallowPlan {
951 roots: Vec<Vec<ObjectId>>,
954 hook_file: Option<tempfile::NamedTempFile>,
958}
959
960impl ShallowPlan {
961 fn none(commands: usize) -> Self {
963 Self {
964 roots: vec![Vec::new(); commands],
965 hook_file: None,
966 }
967 }
968
969 fn new(
972 repo: &Repository,
973 odb: &Odb,
974 request: &PushRequest,
975 policy: &ReceivePolicy,
976 info: &PushShallowInfo,
977 receiver_grafts: &HashSet<ObjectId>,
978 ) -> Result<Self> {
979 if info.is_empty() {
980 return Ok(Self::none(request.commands.len()));
981 }
982 let mut receiver_tips: HashSet<ObjectId> = HashSet::new();
983 super::for_each_advertised_ref(repo, &[], &[], Peel::Yes, |r| {
984 receiver_tips.insert(r.peeled.unwrap_or(r.oid));
985 Ok(std::ops::ControlFlow::Continue(()))
986 })?;
987 receiver_tips.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
988 let graph = repo.odb().commit_graph_chain();
989 let roots = request
990 .commands
991 .iter()
992 .map(|c| match c.new {
993 Some(new) => {
994 info.new_roots(odb, graph.as_deref(), new, receiver_grafts, &receiver_tips)
995 }
996 None => Ok(Vec::new()),
997 })
998 .collect::<crate::error::Result<_>>()?;
999 let hook_file = if policy.shallow_update {
1000 Some(temporary_shallow_file(
1001 repo,
1002 receiver_grafts,
1003 &request.shallow,
1004 )?)
1005 } else {
1006 None
1007 };
1008 Ok(Self { roots, hook_file })
1009 }
1010
1011 fn hook_env(&self) -> Result<Vec<(String, String)>> {
1013 let Some(file) = &self.hook_file else {
1014 return Ok(Vec::new());
1015 };
1016 let path = std::path::absolute(file.path())?;
1017 Ok(vec![(
1018 "GIT_SHALLOW_FILE".to_owned(),
1019 path.to_string_lossy().into_owned(),
1020 )])
1021 }
1022}
1023
1024fn temporary_shallow_file(
1027 repo: &Repository,
1028 receiver_grafts: &HashSet<ObjectId>,
1029 client: &[ObjectId],
1030) -> Result<tempfile::NamedTempFile> {
1031 let mut hexes: Vec<String> = receiver_grafts
1032 .iter()
1033 .chain(client)
1034 .map(ObjectId::to_hex)
1035 .collect();
1036 hexes.sort_unstable();
1037 hexes.dedup();
1038 let dir = crate::shallow::shallow_file_path(repo.git_dir())
1039 .parent()
1040 .map(std::path::Path::to_path_buf)
1041 .unwrap_or_else(|| repo.git_dir().to_path_buf());
1042 let mut file = tempfile::Builder::new()
1043 .prefix("shallow_")
1044 .tempfile_in(dir)?;
1045 for hex in hexes {
1046 writeln!(file, "{hex}")?;
1047 }
1048 file.flush()?;
1049 Ok(file)
1050}
1051
1052#[allow(clippy::too_many_arguments)]
1054fn apply_commands_with_hooks(
1055 repo: &Repository,
1056 request: &PushRequest,
1057 opts: &ServeOptions,
1058 policy: &ReceivePolicy,
1059 hook: &dyn ReceiveHook,
1060 quarantine: Option<&mut ReceiveQuarantine>,
1061 unconnected: &HashMap<ObjectId, Unconnected>,
1062 shallow: &ShallowPlan,
1063 mut sideband_out: Option<&mut dyn Write>,
1064 hook_records: &mut Vec<HookRunRecord>,
1065) -> Result<AppliedPush> {
1066 let lookup_odb = quarantine.as_ref().map(|q| q.odb());
1067 let head_branch = ref_update_policy::head_branch(repo);
1068 let checked_out = ref_update_policy::checked_out_branch(repo);
1069 let branches = ref_update_policy::HeadBranches {
1070 head: head_branch.as_deref(),
1071 checked_out: checked_out.as_deref(),
1072 };
1073 let mut results: Vec<RefUpdateResult> = request
1074 .commands
1075 .iter()
1076 .zip(&shallow.roots)
1077 .map(|(c, roots)| {
1078 if !roots.is_empty() && !policy.shallow_update {
1079 return Ok(c.refused(RefUpdateRefusal::ShallowUpdateNotAllowed));
1080 }
1081 Ok(
1082 match ref_update_policy::validate_ref_update(
1083 repo,
1084 lookup_odb.as_ref(),
1085 c,
1086 policy,
1087 &opts.hidden_refs,
1088 branches,
1089 )? {
1090 Some(reason) => c.refused(reason),
1091 None => match c.new.and_then(|new| unconnected.get(&new)) {
1092 Some(gap) => c.refused(RefUpdateRefusal::Unconnected {
1093 missing: gap.oid,
1094 referrer: gap.referrer,
1095 }),
1096 None => c.accepted(),
1097 },
1098 },
1099 )
1100 })
1101 .collect::<Result<_>>()?;
1102
1103 let atomic = request.caps.contains("atomic");
1104 if atomic && results.iter().any(|r| r.error.is_some()) {
1105 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1106 return Ok(applied_push(results));
1107 }
1108
1109 let accepted: Vec<RefCommand> = request
1110 .commands
1111 .iter()
1112 .zip(&results)
1113 .filter(|(_, r)| r.error.is_none())
1114 .map(|(c, _)| c.clone())
1115 .collect();
1116 let hook_ctx = ReceiveHookContext {
1117 push_options: &request.push_options,
1118 };
1119 if !accepted.is_empty() {
1120 if let Err(refusal) = hook.pre_receive_ctx(
1121 &accepted,
1122 lookup_odb.as_ref().unwrap_or(repo.odb()),
1123 &hook_ctx,
1124 ) {
1125 refuse_remaining(&mut results, &refusal);
1126 return Ok(applied_push(results));
1127 }
1128 }
1129
1130 let scripts = ReceiveHookScripts::new(repo, &request.push_options, shallow.hook_env()?);
1131 let all_cmds: Vec<&RefCommand> = request.commands.iter().collect();
1132 let pre_result = scripts.pre_receive(&all_cmds, quarantine.as_deref());
1133 relay_hook_sideband(&mut sideband_out, &pre_result)?;
1134 hook_records.push(hook_record("pre-receive", &pre_result));
1135 if !pre_result.is_ok() {
1136 refuse_remaining(&mut results, &RefUpdateRefusal::PreReceiveHookDeclined);
1137 return Ok(applied_push(results));
1138 }
1139
1140 if results.iter().all(|r| r.error.is_some()) {
1141 return Ok(applied_push(results));
1142 }
1143
1144 let mut migrated_pack_keeps = Vec::new();
1145 if let Some(q) = quarantine {
1146 migrated_pack_keeps = q.migrate().map_err(ServeError::Repository)?;
1147 repo.odb().invalidate_packs();
1148 }
1149
1150 for (cmd, result) in request.commands.iter().zip(results.iter_mut()) {
1151 if result.error.is_some() {
1152 continue;
1153 }
1154 if let Err(refusal) = hook.update_ctx(cmd, repo.odb(), &hook_ctx) {
1155 result.error = Some(refusal);
1156 continue;
1157 }
1158 let update_result = scripts.update(cmd);
1159 relay_hook_sideband(&mut sideband_out, &update_result)?;
1160 hook_records.push(hook_record("update", &update_result));
1161 if !update_result.is_ok() {
1162 result.error = Some(RefUpdateRefusal::UpdateHookDeclined);
1163 }
1164 }
1165
1166 if atomic && results.iter().any(|r| r.error.is_some()) {
1167 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1168 return Ok(applied_push(results));
1169 }
1170
1171 let new_roots: HashSet<ObjectId> = shallow
1172 .roots
1173 .iter()
1174 .zip(&results)
1175 .filter(|(_, r)| r.error.is_none())
1176 .flat_map(|(roots, _)| roots.iter().copied())
1177 .collect();
1178 if let Err(e) = add_shallow_boundaries(repo.git_dir(), &new_roots) {
1179 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1180 for (roots, result) in shallow.roots.iter().zip(results.iter_mut()) {
1181 if !roots.is_empty() && result.error.is_none() {
1182 result.error = Some(refusal.clone());
1183 }
1184 }
1185 if atomic {
1186 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1187 return Ok(applied_push(results));
1188 }
1189 }
1190
1191 let pending: Vec<(usize, crate::refs::store::RefUpdate)> = request
1192 .commands
1193 .iter()
1194 .zip(results.iter())
1195 .enumerate()
1196 .filter(|(_, (_, r))| r.error.is_none())
1197 .map(|(idx, (cmd, _))| (idx, receive_command_store_update(cmd)))
1198 .collect();
1199
1200 let store = repo.refs();
1201 let ref_lock_wait = store.ref_lock_wait();
1202 if atomic {
1203 let batch: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1204 if let Err(e) =
1205 crate::refs::commit_ref_updates_on_store_with_wait(store, &batch, || ref_lock_wait)
1206 {
1207 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1208 for (idx, _) in pending {
1209 if results[idx].error.is_none() {
1210 results[idx].error = Some(refusal.clone());
1211 }
1212 }
1213 }
1214 } else {
1215 for (idx, message) in commit_receive_ref_updates_on_store(store, pending, ref_lock_wait) {
1216 results[idx].error = Some(RefUpdateRefusal::RefStore(message));
1217 }
1218 }
1219
1220 let successful: Vec<&RefCommand> = request
1221 .commands
1222 .iter()
1223 .zip(results.iter())
1224 .filter(|(_, r)| r.error.is_none())
1225 .map(|(c, _)| c)
1226 .collect();
1227 let mut post_receive_callback_error = None;
1228 let mut post_update_callback_error = None;
1229 if !successful.is_empty() {
1230 let post_result = scripts.post_receive(&successful);
1231 relay_hook_sideband(&mut sideband_out, &post_result)?;
1232 hook_records.push(hook_record("post-receive", &post_result));
1233 post_receive_callback_error = hook
1234 .post_receive(
1235 &successful,
1236 repo.odb(),
1237 &ReceiveHookContext {
1238 push_options: &request.push_options,
1239 },
1240 )
1241 .err();
1242
1243 let pu_result = scripts.post_update(&successful);
1244 relay_hook_sideband(&mut sideband_out, &pu_result)?;
1245 hook_records.push(hook_record("post-update", &pu_result));
1246 post_update_callback_error = hook.post_update(&successful, repo.odb()).err();
1247 }
1248
1249 drop(migrated_pack_keeps);
1250
1251 Ok(AppliedPush {
1252 updates: results,
1253 post_receive_callback_error,
1254 post_update_callback_error,
1255 })
1256}
1257
1258fn refuse_remaining(results: &mut [RefUpdateResult], reason: &RefUpdateRefusal) {
1260 for r in results.iter_mut().filter(|r| r.error.is_none()) {
1261 r.error = Some(reason.clone());
1262 }
1263}
1264
1265fn hook_record(name: &str, result: &HookResult) -> HookRunRecord {
1266 HookRunRecord {
1267 name: name.to_owned(),
1268 ran: result.was_executed(),
1269 exit_code: result.status(),
1270 }
1271}
1272
1273pub(crate) struct ReceiveHookScripts<'a> {
1282 repo: &'a Repository,
1283 receive_env: Vec<(String, String)>,
1285 update_env: Vec<(String, String)>,
1287}
1288
1289impl<'a> ReceiveHookScripts<'a> {
1290 pub(crate) fn new(
1293 repo: &'a Repository,
1294 push_options: &[String],
1295 extra_env: Vec<(String, String)>,
1296 ) -> Self {
1297 let mut receive_env = Vec::with_capacity(push_options.len() + 1 + extra_env.len());
1298 receive_env.push((
1299 "GIT_PUSH_OPTION_COUNT".to_owned(),
1300 push_options.len().to_string(),
1301 ));
1302 receive_env.extend(
1303 push_options
1304 .iter()
1305 .enumerate()
1306 .map(|(i, opt)| (format!("GIT_PUSH_OPTION_{i}"), opt.clone())),
1307 );
1308 receive_env.extend(extra_env.iter().cloned());
1309 Self {
1310 repo,
1311 receive_env,
1312 update_env: extra_env,
1313 }
1314 }
1315
1316 pub(crate) fn pre_receive(
1321 &self,
1322 commands: &[&RefCommand],
1323 quarantine: Option<&ReceiveQuarantine>,
1324 ) -> HookResult {
1325 let mut env = self.receive_env.clone();
1326 if let Some(q) = quarantine {
1327 env.extend(q.hook_env());
1328 }
1329 self.run_with_commands("pre-receive", commands, &env)
1330 }
1331
1332 pub(crate) fn update(&self, cmd: &RefCommand) -> HookResult {
1334 let algo = self.repo.odb().hash_algo();
1335 let hex = |oid: Option<ObjectId>| oid.unwrap_or_else(|| ObjectId::null(algo)).to_hex();
1336 let (old, new) = (hex(cmd.old), hex(cmd.new));
1337 let args = [cmd.refname.as_str(), old.as_str(), new.as_str()];
1338 run_hook_in_git_dir(self.repo, "update", &args, None, &pairs(&self.update_env))
1339 }
1340
1341 pub(crate) fn post_receive(&self, commands: &[&RefCommand]) -> HookResult {
1343 self.run_with_commands("post-receive", commands, &self.receive_env)
1344 }
1345
1346 pub(crate) fn post_update(&self, commands: &[&RefCommand]) -> HookResult {
1348 let args: Vec<&str> = commands.iter().map(|c| c.refname.as_str()).collect();
1349 run_hook_in_git_dir(
1350 self.repo,
1351 "post-update",
1352 &args,
1353 None,
1354 &pairs(&self.update_env),
1355 )
1356 }
1357
1358 fn run_with_commands(
1359 &self,
1360 hook_name: &str,
1361 commands: &[&RefCommand],
1362 env: &[(String, String)],
1363 ) -> HookResult {
1364 let zero = ObjectId::null(self.repo.odb().hash_algo());
1365 let stdin: String = commands
1366 .iter()
1367 .map(|c| {
1368 format!(
1369 "{} {} {}\n",
1370 c.old.unwrap_or(zero).to_hex(),
1371 c.new.unwrap_or(zero).to_hex(),
1372 c.refname
1373 )
1374 })
1375 .collect();
1376 run_hook_in_git_dir(
1377 self.repo,
1378 hook_name,
1379 &[],
1380 Some(stdin.as_bytes()),
1381 &pairs(env),
1382 )
1383 }
1384}
1385
1386fn pairs(env: &[(String, String)]) -> Vec<(&str, &str)> {
1387 env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
1388}
1389
1390fn receive_updates_eligible_for_initial_packed(updates: &[crate::refs::store::RefUpdate]) -> bool {
1391 use crate::refs::store::{Expected, RawRef};
1392 updates.iter().all(|update| {
1393 matches!(update.new_value, Some(RawRef::Direct(_)))
1394 && matches!(update.expected, Expected::Missing)
1395 && !update.flags.log_only
1396 && update.reflog.is_none()
1397 })
1398}
1399
1400fn commit_receive_ref_updates_on_store(
1406 store: &dyn crate::refs::store::RefStore,
1407 pending: Vec<(usize, crate::refs::store::RefUpdate)>,
1408 ref_lock_wait: crate::lockfile::LockWait,
1409) -> Vec<(usize, String)> {
1410 if pending.is_empty() {
1411 return Vec::new();
1412 }
1413 let updates: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1414 let batch_ok = if receive_updates_eligible_for_initial_packed(&updates) {
1415 crate::refs::commit_initial_ref_updates_on_store_with_wait(store, updates.clone(), || {
1416 ref_lock_wait
1417 })
1418 } else {
1419 crate::refs::commit_ref_updates_on_store_with_wait(store, &updates, || ref_lock_wait)
1420 };
1421 if batch_ok.is_ok() {
1422 return Vec::new();
1423 }
1424 let indices: Vec<_> = pending.iter().map(|(idx, _)| *idx).collect();
1425 match crate::refs::commit_ref_updates_non_atomic_on_store(store, updates) {
1426 Ok(outcomes) => indices
1427 .into_iter()
1428 .zip(outcomes)
1429 .filter_map(|(idx, outcome)| outcome.result.err().map(|e| (idx, e.to_string())))
1430 .collect(),
1431 Err(e) => indices
1432 .into_iter()
1433 .map(|idx| (idx, e.to_string()))
1434 .collect(),
1435 }
1436}
1437
1438fn receive_command_store_update(cmd: &RefCommand) -> crate::refs::store::RefUpdate {
1439 let mut flags = crate::refs::store::RefUpdateFlags::default();
1440 if cmd.refname == "HEAD" {
1441 flags.no_deref = true;
1442 }
1443 crate::refs::store::RefUpdate {
1444 name: cmd.refname.clone(),
1445 new_value: cmd.new.map(crate::refs::store::RawRef::Direct),
1446 expected: match cmd.old {
1447 Some(oid) => crate::refs::store::Expected::Oid(oid),
1448 None => crate::refs::store::Expected::Missing,
1449 },
1450 reflog: None,
1451 flags,
1452 }
1453}
1454
1455fn relay_hook_sideband(out: &mut Option<&mut dyn Write>, result: &HookResult) -> Result<()> {
1457 let (Some(w), Some(output)) = (out.as_mut(), result.output()) else {
1458 return Ok(());
1459 };
1460 for chunk in output
1461 .stdout
1462 .chunks(65515)
1463 .chain(output.stderr.chunks(65515))
1464 {
1465 crate::pkt_line::write_sideband_packet(*w, 2, chunk).map_err(ServeError::Io)?;
1466 }
1467 Ok(())
1468}
1469
1470fn build_report(
1473 unpack_error: Option<&UnpackError>,
1474 updates: &[RefUpdateResult],
1475) -> Result<Vec<u8>> {
1476 let mut report = Vec::new();
1477 match unpack_error {
1478 None => write_line(&mut report, "unpack ok")?,
1479 Some(e) => write_line(&mut report, &format!("unpack {}", one_line(&e.to_string())))?,
1480 }
1481 for u in updates {
1482 match &u.error {
1483 None => write_line(&mut report, &format!("ok {}", u.refname))?,
1484 Some(reason) => write_line(
1485 &mut report,
1486 &format!("ng {} {}", u.refname, one_line(&reason.to_string())),
1487 )?,
1488 }
1489 }
1490 write_flush(&mut report)?;
1491 Ok(report)
1492}
1493
1494fn one_line(msg: &str) -> String {
1496 msg.lines().next().unwrap_or_default().trim().to_owned()
1497}
1498
1499struct SpooledPack {
1501 file: tempfile::NamedTempFile,
1502 header: [u8; 12],
1504 bytes_received: u64,
1506}
1507
1508impl SpooledPack {
1509 fn bytes_received(&self) -> u64 {
1510 self.bytes_received
1511 }
1512
1513 fn object_count(&self) -> u32 {
1514 u32::from_be_bytes([
1515 self.header[8],
1516 self.header[9],
1517 self.header[10],
1518 self.header[11],
1519 ])
1520 }
1521}
1522
1523fn spool_push_pack(
1531 input: &mut dyn Read,
1532 odb: &Odb,
1533 max_bytes: Option<u64>,
1534 stop: &CancelSignal<'_>,
1535) -> std::result::Result<SpooledPack, UnpackError> {
1536 let hash = odb.hash_algo();
1537 let mut file = tempfile::Builder::new()
1538 .prefix("tmp_pack_")
1539 .tempfile_in(odb.objects_dir().join("pack"))?;
1540 let mut scanner = PackStreamScanner::new(hash);
1541 let mut header = [0u8; 12];
1542 let mut chunk = vec![0u8; SPOOL_CHUNK];
1543 let mut written = 0u64;
1544 let mut pack_len: Option<u64> = None;
1545 let mut out = BufWriter::with_capacity(SPOOL_CHUNK, file.as_file_mut());
1546 loop {
1547 stop.poll().map_err(|_| UnpackError::Cancelled)?;
1548 let want = match pack_len {
1549 Some(len) if written >= len => break,
1550 Some(len) => usize::try_from(len - written).map_or(chunk.len(), |n| n.min(chunk.len())),
1551 None => chunk.len(),
1552 };
1553 let n = read_some(input, &mut chunk[..want])?;
1554 if n == 0 {
1555 return Err(UnpackError::Truncated);
1556 }
1557 let mut data = &chunk[..n];
1558 if pack_len.is_none() {
1559 scanner.feed(data)?;
1560 if scanner.is_complete() {
1561 let len = scanner
1562 .pack_body_byte_len()
1563 .saturating_add(hash.len() as u64);
1564 let keep = usize::try_from(len.saturating_sub(written)).map_or(n, |k| k.min(n));
1565 data = &data[..keep];
1566 pack_len = Some(len);
1567 }
1568 }
1569 if let Some(limit) = max_bytes {
1570 let total = pack_len.unwrap_or(written.saturating_add(data.len() as u64));
1571 if total > limit {
1572 return Err(UnpackError::TooLarge { limit });
1573 }
1574 }
1575 if let Some(missing) = usize::try_from(written)
1576 .ok()
1577 .and_then(|start| header.get_mut(start..))
1578 {
1579 let take = missing.len().min(data.len());
1580 missing[..take].copy_from_slice(&data[..take]);
1581 }
1582 out.write_all(data)?;
1583 written += data.len() as u64;
1584 }
1585 out.flush()?;
1586 drop(out);
1587 Ok(SpooledPack {
1588 file,
1589 header,
1590 bytes_received: written,
1591 })
1592}
1593
1594fn read_some(input: &mut dyn Read, buf: &mut [u8]) -> std::io::Result<usize> {
1596 loop {
1597 match input.read(buf) {
1598 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1599 other => return other,
1600 }
1601 }
1602}
1603
1604fn check_received_objects(
1609 repo: &Repository,
1610 odb: &Odb,
1611 indexed: Option<&HashSet<ObjectId>>,
1612 cfg: &ConfigSet,
1613 fsck: &ObjectFsckPolicy,
1614) -> std::result::Result<(), UnpackError> {
1615 let checker = fsck.checker(odb.hash_algo(), repo.git_dir(), repo.environment())?;
1616 let mut received: Vec<ObjectId> = match indexed {
1617 Some(ids) => ids.iter().copied().collect(),
1618 None => odb
1619 .enumerate_local_loose_objects()?
1620 .into_iter()
1621 .map(|(oid, _)| oid)
1622 .collect(),
1623 };
1624 received.sort_unstable();
1625 let parallel = repo
1626 .thread_policy()
1627 .try_resolve(ParallelOp::IndexPack, cfg)
1628 .map_err(crate::error::Error::from)?;
1629 let verdicts = try_par_map(&received, parallel, received.len(), |oid| {
1630 if odb.read_info(oid)?.kind == ObjectKind::Blob {
1631 return Ok(None);
1632 }
1633 let obj = odb.read(oid)?;
1634 Ok(checker
1635 .check(oid, obj.kind, &obj.data)
1636 .err()
1637 .map(|error| RejectedObject {
1638 oid: *oid,
1639 kind: obj.kind,
1640 error,
1641 }))
1642 })
1643 .map_err(|ParallelHashError::Task(e): ParallelHashError<crate::error::Error>| e)?;
1644 match verdicts.into_iter().flatten().next() {
1645 Some(rejected) => Err(UnpackError::Rejected(rejected)),
1646 None => Ok(()),
1647 }
1648}
1649
1650fn unconnected_tips(
1656 repo: &Repository,
1657 odb: &Odb,
1658 received: &HashSet<ObjectId>,
1659 existing: &HashSet<ObjectId>,
1660 commands: &[RefCommand],
1661 shallow_boundaries: &HashSet<ObjectId>,
1662 promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1663) -> std::result::Result<HashMap<ObjectId, Unconnected>, UnpackError> {
1664 let mut tips: Vec<ObjectId> = commands.iter().filter_map(|c| c.new).collect();
1665 tips.sort_unstable();
1666 tips.dedup();
1667 if tips.is_empty() {
1668 return Ok(HashMap::new());
1669 }
1670 let receiver_grafts = load_shallow_boundaries(repo.git_dir());
1671 let graph = repo.odb().commit_graph_chain();
1672 let pushed = PushedObjects {
1673 odb,
1674 repo_odb: repo.odb(),
1675 graph: graph.as_deref(),
1676 received,
1677 existing,
1678 shallow_boundaries,
1679 receiver_grafts: &receiver_grafts,
1680 promises,
1681 };
1682 let gaps = pushed.unconnected_tips(&tips)?;
1683 Ok(tips
1684 .into_iter()
1685 .zip(gaps)
1686 .filter_map(|(tip, gap)| Some((tip, gap?)))
1687 .collect())
1688}
1689
1690fn existing_ref_values(repo: &Repository) -> Result<HashSet<ObjectId>> {
1692 let mut existing = super::advertised_object_ids_from_repo(repo, &[], &[], super::Peel::Yes)?;
1693 existing.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
1694 Ok(existing)
1695}
1696
1697fn ingest_spooled_pack(
1709 odb: &Odb,
1710 pack: SpooledPack,
1711 cfg: &ConfigSet,
1712 shallow_boundaries: &HashSet<ObjectId>,
1713 promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1714) -> std::result::Result<Option<HashSet<ObjectId>>, UnpackError> {
1715 if should_use_unpack_objects(&pack.header, cfg) {
1716 let mut reader = BufReader::with_capacity(SPOOL_CHUNK, pack.file.reopen()?);
1717 let opts = UnpackOptions {
1718 quiet: true,
1719 strict: true,
1720 shallow_boundaries: shallow_boundaries.clone(),
1721 ..UnpackOptions::default()
1722 };
1723 match promises {
1724 Some(promises) => {
1725 let promised =
1726 |oid: &ObjectId, referrer: &ObjectId| promises.is_promised(oid, Some(referrer));
1727 crate::unpack_objects::unpack_objects_with_promises(
1728 &mut reader,
1729 odb,
1730 &opts,
1731 &promised,
1732 )?;
1733 }
1734 None => {
1735 unpack_objects(&mut reader, odb, &opts)?;
1736 }
1737 }
1738 return Ok(None);
1739 }
1740 let path = pack.file.into_temp_path();
1741 let installed = install_pack_path(
1742 &path,
1743 odb,
1744 &IngestPackOptions {
1745 fix_thin: true,
1746 skip_post_index_verify: true,
1749 merged_config: Some(cfg.clone()),
1750 ..Default::default()
1751 },
1752 )?;
1753 Ok(Some(installed.object_ids))
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758 use super::*;
1759
1760 #[test]
1761 fn report_lists_unpack_status_then_each_ref() {
1762 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
1763 .unwrap_or_else(|_| ObjectId::zero());
1764 let updates = vec![
1765 RefUpdateResult {
1766 refname: "refs/heads/main".into(),
1767 old: None,
1768 new: Some(oid),
1769 error: None,
1770 },
1771 RefUpdateResult {
1772 refname: "refs/heads/topic".into(),
1773 old: Some(oid),
1774 new: None,
1775 error: Some(RefUpdateRefusal::DeletionProhibited),
1776 },
1777 ];
1778 let report = build_report(None, &updates).unwrap_or_default();
1779 assert_eq!(
1780 String::from_utf8_lossy(&report),
1781 "000eunpack ok\n0017ok refs/heads/main\n002cng refs/heads/topic deletion prohibited\n0000"
1782 );
1783 }
1784
1785 #[test]
1786 fn report_renders_typed_unpack_error_and_refusal() {
1787 let updates = vec![RefUpdateResult {
1788 refname: "refs/heads/main".into(),
1789 old: None,
1790 new: None,
1791 error: Some(RefUpdateRefusal::UnpackerError),
1792 }];
1793 let report = build_report(Some(&UnpackError::Truncated), &updates).unwrap_or_default();
1794 assert_eq!(
1795 String::from_utf8_lossy(&report),
1796 "0029unpack unexpected end of pack stream\n0026ng refs/heads/main unpacker error\n0000"
1797 );
1798 }
1799
1800 #[test]
1801 fn refusals_render_the_reasons_git_clients_expect() {
1802 let cases = [
1803 (RefUpdateRefusal::InvalidRefName, "funny refname"),
1804 (RefUpdateRefusal::HiddenRef, "deny updating a hidden ref"),
1805 (RefUpdateRefusal::StaleInfo, "stale info"),
1806 (RefUpdateRefusal::DeletionProhibited, "deletion prohibited"),
1807 (
1808 RefUpdateRefusal::CurrentBranchDeletion,
1809 "deletion of the current branch prohibited",
1810 ),
1811 (
1812 RefUpdateRefusal::CurrentBranch,
1813 "branch is currently checked out",
1814 ),
1815 (
1816 RefUpdateRefusal::MissingObjects,
1817 "missing necessary objects",
1818 ),
1819 (
1820 RefUpdateRefusal::Unconnected {
1821 missing: ObjectId::zero(),
1822 referrer: ObjectId::zero(),
1823 },
1824 "missing necessary objects",
1825 ),
1826 (RefUpdateRefusal::NonFastForward, "non-fast-forward"),
1827 (RefUpdateRefusal::BadRef, "bad ref"),
1828 (
1829 RefUpdateRefusal::FastForwardCheckFailed,
1830 "unable to verify fast-forward",
1831 ),
1832 (
1833 RefUpdateRefusal::ShallowUpdateNotAllowed,
1834 "shallow update not allowed",
1835 ),
1836 (RefUpdateRefusal::UnpackerError, "unpacker error"),
1837 (
1838 RefUpdateRefusal::AtomicTransactionFailed,
1839 "atomic transaction failed",
1840 ),
1841 (
1842 RefUpdateRefusal::PreReceiveHookDeclined,
1843 "pre-receive hook declined",
1844 ),
1845 (RefUpdateRefusal::UpdateHookDeclined, "hook declined"),
1846 (RefUpdateRefusal::Policy("frozen".into()), "frozen"),
1847 (
1848 RefUpdateRefusal::TargetBranch {
1849 target: "main".into(),
1850 },
1851 "main is the target branch; it changes only through integrate",
1852 ),
1853 (
1854 RefUpdateRefusal::RefStore("locked".into()),
1855 "failed to update ref: locked",
1856 ),
1857 ];
1858 for (refusal, wire) in cases {
1859 assert_eq!(refusal.to_string(), wire);
1860 }
1861 }
1862
1863 struct NoReadPastEnd<'a> {
1866 data: &'a [u8],
1867 }
1868
1869 impl Read for NoReadPastEnd<'_> {
1870 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1871 if self.data.is_empty() {
1872 return Err(std::io::Error::other("read past the end of the pack"));
1873 }
1874 let n = buf.len().min(self.data.len());
1875 buf[..n].copy_from_slice(&self.data[..n]);
1876 self.data = &self.data[n..];
1877 Ok(n)
1878 }
1879 }
1880
1881 fn sample_pack() -> Vec<u8> {
1882 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
1883 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
1884 let mut seed = 0x2545_f491_u32;
1885 let big: Vec<u8> = (0..(SPOOL_CHUNK * 3))
1887 .map(|_| {
1888 seed ^= seed << 13;
1889 seed ^= seed >> 17;
1890 seed ^= seed << 5;
1891 seed as u8
1892 })
1893 .collect();
1894 pb.add_full(PK::Blob, &big);
1895 pb.add_full(PK::Blob, b"small\n");
1896 pb.build().bytes
1897 }
1898
1899 fn test_odb(dir: &std::path::Path) -> Odb {
1900 let objects = dir.join("objects");
1901 std::fs::create_dir_all(objects.join("pack")).unwrap_or_default();
1902 Odb::new(&objects)
1903 }
1904
1905 #[test]
1906 fn spool_stops_at_the_trailer_without_reading_further() {
1907 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1908 let odb = test_odb(tmp.path());
1909 let pack = sample_pack();
1910 let mut input = NoReadPastEnd { data: &pack };
1911 let spooled = spool_push_pack(&mut input, &odb, None, &CancelSignal::never())
1912 .unwrap_or_else(|e| panic!("{e}"));
1913 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1914 assert_eq!(&spooled.header[..], &pack[..12]);
1915 }
1916
1917 #[test]
1918 fn spool_keeps_only_the_pack_when_more_bytes_follow() {
1919 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1920 let odb = test_odb(tmp.path());
1921 let pack = sample_pack();
1922 let mut stream = pack.clone();
1923 stream.extend_from_slice(b"trailing junk");
1924 let spooled = spool_push_pack(&mut stream.as_slice(), &odb, None, &CancelSignal::never())
1925 .unwrap_or_else(|e| panic!("{e}"));
1926 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1927 }
1928
1929 #[test]
1930 fn spool_reports_typed_errors() {
1931 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1932 let odb = test_odb(tmp.path());
1933 let pack = sample_pack();
1934
1935 let truncated = &pack[..pack.len() - 5];
1936 let never = CancelSignal::never();
1937 assert!(matches!(
1938 spool_push_pack(&mut &truncated[..], &odb, None, &never),
1939 Err(UnpackError::Truncated)
1940 ));
1941
1942 let limit = (pack.len() - 1) as u64;
1943 assert!(matches!(
1944 spool_push_pack(&mut pack.as_slice(), &odb, Some(limit), &never),
1945 Err(UnpackError::TooLarge { limit: l }) if l == limit
1946 ));
1947 assert!(
1948 spool_push_pack(&mut pack.as_slice(), &odb, Some(pack.len() as u64), &never).is_ok()
1949 );
1950
1951 assert!(matches!(
1952 spool_push_pack(&mut &b"JUNK\0\0\0\x02\0\0\0\x01"[..], &odb, None, &never),
1953 Err(UnpackError::Malformed(PackStreamScanError::BadSignature))
1954 ));
1955
1956 let leftovers: Vec<_> = std::fs::read_dir(odb.objects_dir().join("pack"))
1957 .map(|rd| rd.filter_map(|e| e.ok()).collect())
1958 .unwrap_or_default();
1959 assert!(
1960 leftovers.is_empty(),
1961 "failed spools must not leave temp files"
1962 );
1963 }
1964
1965 #[test]
1966 fn deny_current_branch_values() {
1967 let policy_for = |value: Option<&str>| {
1968 let mut config = ConfigSet::new();
1969 if let Some(v) = value {
1970 let _ = config.add_command_override("receive.denyCurrentBranch", v);
1971 }
1972 ReceivePolicy::from_config(&config).deny_current_branch
1973 };
1974 assert!(policy_for(None));
1975 assert!(!policy_for(Some("ignore")));
1976 assert!(!policy_for(Some("updateInstead")));
1977 assert!(policy_for(Some("refuse")));
1978 }
1979
1980 #[test]
1981 fn shallow_update_is_off_unless_configured() {
1982 assert!(!ReceivePolicy::default().shallow_update);
1983 let mut config = ConfigSet::new();
1984 let _ = config.add_command_override("receive.shallowUpdate", "true");
1985 assert!(ReceivePolicy::from_config(&config).shallow_update);
1986 }
1987
1988 #[test]
1989 fn deny_delete_current_values() {
1990 let policy_for = |value: Option<&str>| {
1991 let mut config = ConfigSet::new();
1992 if let Some(v) = value {
1993 let _ = config.add_command_override("receive.denyDeleteCurrent", v);
1994 }
1995 ReceivePolicy::from_config(&config).deny_delete_current
1996 };
1997 assert!(policy_for(None));
1998 assert!(!policy_for(Some("ignore")));
1999 assert!(!policy_for(Some("warn")));
2000 assert!(!policy_for(Some("false")));
2001 assert!(policy_for(Some("updateInstead")));
2003 assert!(policy_for(Some("refuse")));
2004 assert!(ReceivePolicy::default().deny_delete_current);
2005 }
2006}