1use std::collections::{HashMap, HashSet};
4use std::io::{BufReader, BufWriter, Read, Write};
5
6use super::connectivity::{PushedObjects, Unconnected};
7
8use super::{
9 advertised_refs, check_object_format, parse_oid, read_packet, ref_update_policy, write_flush,
10 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
437#[derive(Debug, Clone, Copy, Default)]
439pub struct ReceiveHookContext<'a> {
440 pub push_options: &'a [String],
442}
443
444impl ReceiveHook for () {}
445
446impl<F> ReceiveHook for F
447where
448 F: Fn(&RefCommand, &Odb) -> std::result::Result<(), RefUpdateRefusal>,
449{
450 fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
451 self(command, odb)
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq)]
457pub struct HookRunRecord {
458 pub name: String,
460 pub ran: bool,
462 pub exit_code: Option<i32>,
464}
465
466#[derive(Debug, Default)]
468pub struct ReceiveOutcome {
469 pub unpack_error: Option<UnpackError>,
471 pub updates: Vec<RefUpdateResult>,
473 pub hooks: Vec<HookRunRecord>,
475 pub push_options: Vec<String>,
477}
478
479pub fn receive_pack(
488 repo: &Repository,
489 input: &mut dyn Read,
490 output: &mut dyn Write,
491 opts: &ServeOptions,
492 policy: &ReceivePolicy,
493) -> Result<ReceiveOutcome> {
494 receive_pack_with_hook(repo, input, output, opts, policy, &())
495}
496
497pub fn receive_pack_with_hook(
551 repo: &Repository,
552 input: &mut dyn Read,
553 output: &mut dyn Write,
554 opts: &ServeOptions,
555 policy: &ReceivePolicy,
556 hook: &dyn ReceiveHook,
557) -> Result<ReceiveOutcome> {
558 receive_pack_with_hook_cancellable(repo, input, output, opts, policy, hook, &NeverCancel)
559}
560
561pub fn receive_pack_with_hook_cancellable(
571 repo: &Repository,
572 input: &mut dyn Read,
573 output: &mut dyn Write,
574 opts: &ServeOptions,
575 policy: &ReceivePolicy,
576 hook: &dyn ReceiveHook,
577 cancel: &dyn Cancel,
578) -> Result<ReceiveOutcome> {
579 let stop = CancelSignal::new(cancel);
580 stop.poll().map_err(|_| ServeError::Cancelled)?;
581 if opts.advertise_refs || !opts.stateless_rpc {
582 if opts.protocol == ProtocolVersion::V1 {
583 write_line(output, "version 1")?;
584 }
585 write_advertisement(repo, output, opts)?;
586 output.flush()?;
587 if opts.advertise_refs {
588 return Ok(ReceiveOutcome::default());
589 }
590 }
591
592 let Some(request) = read_commands(repo, input)? else {
593 return Ok(ReceiveOutcome::default());
594 };
595
596 let cfg = repo
597 .config()
598 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
599 let max_input = max_input_size_from_config(cfg.as_ref());
600 let sideband = request.caps.contains("side-band-64k");
601
602 let needs_pack = request.commands.iter().any(|c| c.new.is_some());
603 let mut quarantine = if needs_pack {
604 Some(ReceiveQuarantine::create(repo.odb(), repo.git_dir())?)
605 } else {
606 None
607 };
608
609 let receiver_grafts = if request.shallow.is_empty() {
610 HashSet::new()
611 } else {
612 load_shallow_boundaries(repo.git_dir())
613 };
614 let mut shallow_info =
615 PushShallowInfo::classify(&request.shallow, repo.odb(), &receiver_grafts)?;
616 let client_boundaries: HashSet<ObjectId> = request.shallow.iter().copied().collect();
617
618 let promises = if crate::promisor::repo_treats_promisor_packs(repo.git_dir(), cfg.as_ref()) {
621 Some(super::promised_objects::PromisorObjects::new(repo)?)
622 } else {
623 None
624 };
625 let existing = if needs_pack {
627 existing_ref_values(repo)?
628 } else {
629 HashSet::new()
630 };
631 let mut unconnected: HashMap<ObjectId, Unconnected> = HashMap::new();
633 let unpack_error = match quarantine.as_ref() {
634 Some(q) => {
635 let odb = q.odb();
636 spool_push_pack(input, &odb, max_input, &stop)
637 .and_then(|pack| {
638 ingest_spooled_pack(
639 &odb,
640 pack,
641 cfg.as_ref(),
642 &client_boundaries,
643 promises.as_ref(),
644 )
645 })
646 .and_then(|indexed| {
647 if let Some(fsck) = &policy.fsck_objects {
648 check_received_objects(repo, &odb, indexed.as_ref(), cfg.as_ref(), fsck)?;
649 }
650 let received = match indexed {
653 Some(received) => received,
654 None => odb
655 .enumerate_local_loose_objects()?
656 .into_iter()
657 .map(|(oid, _)| oid)
658 .collect(),
659 };
660 unconnected = unconnected_tips(
661 repo,
662 &odb,
663 &received,
664 &existing,
665 &request.commands,
666 &client_boundaries,
667 promises.as_ref(),
668 )?;
669 Ok(())
670 })
671 .err()
672 }
673 None => None,
674 };
675 if matches!(unpack_error, Some(UnpackError::Cancelled)) {
676 return Err(ServeError::Cancelled);
677 }
678
679 let shallow = match quarantine.as_ref() {
680 Some(q) if unpack_error.is_none() => {
681 shallow_info.retain_received(&q.odb())?;
682 ShallowPlan::new(
683 repo,
684 &q.odb(),
685 &request,
686 policy,
687 &shallow_info,
688 &receiver_grafts,
689 )?
690 }
691 _ => ShallowPlan::none(request.commands.len()),
692 };
693
694 let mut hook_records = Vec::new();
695 let updates = if unpack_error.is_some() {
696 request
697 .commands
698 .iter()
699 .map(|c| c.refused(RefUpdateRefusal::UnpackerError))
700 .collect()
701 } else {
702 stop.poll().map_err(|_| ServeError::Cancelled)?;
703 apply_commands_with_hooks(
704 repo,
705 &request,
706 opts,
707 policy,
708 hook,
709 quarantine.as_mut(),
710 &unconnected,
711 &shallow,
712 sideband.then_some(&mut *output),
713 &mut hook_records,
714 )?
715 };
716
717 if sideband {
718 if let Some(text) = hook.message(&updates) {
719 write_progress_text(output, &text)?;
720 }
721 }
722 if request.caps.contains("report-status") || request.caps.contains("report-status-v2") {
723 let report = build_report(unpack_error.as_ref(), &updates)?;
724 if sideband {
725 crate::pkt_line::write_sideband_channel1_64k(&mut &mut *output, &report)?;
726 write_flush(output)?;
727 } else {
728 output.write_all(&report)?;
729 }
730 }
731 output.flush()?;
732
733 Ok(ReceiveOutcome {
734 unpack_error,
735 updates,
736 hooks: hook_records,
737 push_options: request.push_options.clone(),
738 })
739}
740
741fn write_progress_text(output: &mut dyn Write, text: &str) -> Result<()> {
744 for line in text.lines() {
745 let line = format!("{line}\n");
746 for chunk in line.as_bytes().chunks(65515) {
747 crate::pkt_line::write_sideband_packet(&mut *output, 2, chunk)
748 .map_err(ServeError::Io)?;
749 }
750 }
751 Ok(())
752}
753
754fn write_advertisement(
756 repo: &Repository,
757 output: &mut dyn Write,
758 opts: &ServeOptions,
759) -> Result<()> {
760 let mut caps: Vec<String> = CAPABILITIES.iter().map(|c| (*c).to_owned()).collect();
761 let config = repo
762 .config()
763 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
764 if config
765 .get_bool("receive.advertisepushoptions")
766 .and_then(|r| r.ok())
767 .unwrap_or(false)
768 {
769 caps.push("push-options".to_owned());
770 }
771 caps.push(format!("object-format={}", repo.odb().hash_algo().name()));
772 if let Some(path) = &opts.push_bundle {
773 caps.push(format!("{}={path}", super::PUSH_BUNDLE_CAPABILITY));
774 }
775 if !opts.agent.is_empty() {
776 caps.push(format!("agent={}", opts.agent));
777 }
778 let caps = caps.join(" ");
779
780 let refs = advertised_refs(repo, &opts.hidden_refs, &[], Peel::No)?;
781 if refs.is_empty() {
782 let zero = ObjectId::null(repo.odb().hash_algo());
783 write_line(
784 output,
785 &format!("{} capabilities^{{}}\0{caps}", zero.to_hex()),
786 )?;
787 }
788 for (i, r) in refs.iter().enumerate() {
789 if i == 0 {
790 write_line(output, &format!("{} {}\0{caps}", r.oid.to_hex(), r.name))?;
791 } else {
792 write_line(output, &format!("{} {}", r.oid.to_hex(), r.name))?;
793 }
794 }
795 write_flush(output)
796}
797
798struct PushRequest {
800 commands: Vec<RefCommand>,
801 caps: HashSet<String>,
802 push_options: Vec<String>,
803 shallow: Vec<ObjectId>,
805}
806
807fn read_commands(repo: &Repository, input: &mut dyn Read) -> Result<Option<PushRequest>> {
813 let mut commands = Vec::new();
814 let mut caps = HashSet::new();
815 let mut shallow = Vec::new();
816 loop {
817 match read_packet(input)? {
818 None | Some(Packet::Flush) => break,
819 Some(Packet::Data(line)) => {
820 if let Some(hex) = line.strip_prefix("shallow ") {
821 shallow.push(parse_oid(repo, hex)?);
822 continue;
823 }
824 let (line, line_caps) = line.split_once('\0').unwrap_or((line.as_str(), ""));
825 if commands.is_empty() {
826 caps.extend(
827 line_caps
828 .split(' ')
829 .filter(|c| !c.is_empty())
830 .map(str::to_owned),
831 );
832 }
833 let mut parts = line.splitn(3, ' ');
834 let (Some(old), Some(new), Some(refname)) =
835 (parts.next(), parts.next(), parts.next())
836 else {
837 return Err(ServeError::Protocol(format!("malformed command '{line}'")));
838 };
839 let old = parse_oid(repo, old)?;
840 let new = parse_oid(repo, new)?;
841 commands.push(RefCommand {
842 old: (!old.is_zero()).then_some(old),
843 new: (!new.is_zero()).then_some(new),
844 refname: refname.to_owned(),
845 });
846 }
847 Some(other) => {
848 return Err(ServeError::Protocol(format!(
849 "unexpected packet in command list: {other:?}"
850 )))
851 }
852 }
853 }
854 if commands.is_empty() {
855 return Ok(None);
856 }
857 let mut push_options = Vec::new();
858 if caps.contains("push-options") {
859 loop {
860 match read_packet(input)? {
861 None | Some(Packet::Flush) => break,
862 Some(Packet::Data(line)) => {
863 push_options.push(line);
864 }
865 Some(other) => {
866 return Err(ServeError::Protocol(format!(
867 "unexpected packet in push options: {other:?}"
868 )))
869 }
870 }
871 }
872 }
873 let format = caps.iter().find_map(|c| c.strip_prefix("object-format="));
874 check_object_format(repo, format)?;
875 Ok(Some(PushRequest {
876 commands,
877 caps,
878 push_options,
879 shallow,
880 }))
881}
882
883struct ShallowPlan {
885 roots: Vec<Vec<ObjectId>>,
888 hook_file: Option<tempfile::NamedTempFile>,
892}
893
894impl ShallowPlan {
895 fn none(commands: usize) -> Self {
897 Self {
898 roots: vec![Vec::new(); commands],
899 hook_file: None,
900 }
901 }
902
903 fn new(
906 repo: &Repository,
907 odb: &Odb,
908 request: &PushRequest,
909 policy: &ReceivePolicy,
910 info: &PushShallowInfo,
911 receiver_grafts: &HashSet<ObjectId>,
912 ) -> Result<Self> {
913 if info.is_empty() {
914 return Ok(Self::none(request.commands.len()));
915 }
916 let mut receiver_tips: HashSet<ObjectId> = advertised_refs(repo, &[], &[], Peel::Yes)?
917 .into_iter()
918 .map(|r| r.peeled.unwrap_or(r.oid))
919 .collect();
920 receiver_tips.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
921 let graph = repo.odb().commit_graph_chain();
922 let roots = request
923 .commands
924 .iter()
925 .map(|c| match c.new {
926 Some(new) => {
927 info.new_roots(odb, graph.as_deref(), new, receiver_grafts, &receiver_tips)
928 }
929 None => Ok(Vec::new()),
930 })
931 .collect::<crate::error::Result<_>>()?;
932 let hook_file = if policy.shallow_update {
933 Some(temporary_shallow_file(
934 repo,
935 receiver_grafts,
936 &request.shallow,
937 )?)
938 } else {
939 None
940 };
941 Ok(Self { roots, hook_file })
942 }
943
944 fn hook_env(&self) -> Result<Vec<(String, String)>> {
946 let Some(file) = &self.hook_file else {
947 return Ok(Vec::new());
948 };
949 let path = std::path::absolute(file.path())?;
950 Ok(vec![(
951 "GIT_SHALLOW_FILE".to_owned(),
952 path.to_string_lossy().into_owned(),
953 )])
954 }
955}
956
957fn temporary_shallow_file(
960 repo: &Repository,
961 receiver_grafts: &HashSet<ObjectId>,
962 client: &[ObjectId],
963) -> Result<tempfile::NamedTempFile> {
964 let mut hexes: Vec<String> = receiver_grafts
965 .iter()
966 .chain(client)
967 .map(ObjectId::to_hex)
968 .collect();
969 hexes.sort_unstable();
970 hexes.dedup();
971 let dir = crate::shallow::shallow_file_path(repo.git_dir())
972 .parent()
973 .map(std::path::Path::to_path_buf)
974 .unwrap_or_else(|| repo.git_dir().to_path_buf());
975 let mut file = tempfile::Builder::new()
976 .prefix("shallow_")
977 .tempfile_in(dir)?;
978 for hex in hexes {
979 writeln!(file, "{hex}")?;
980 }
981 file.flush()?;
982 Ok(file)
983}
984
985#[allow(clippy::too_many_arguments)]
987fn apply_commands_with_hooks(
988 repo: &Repository,
989 request: &PushRequest,
990 opts: &ServeOptions,
991 policy: &ReceivePolicy,
992 hook: &dyn ReceiveHook,
993 quarantine: Option<&mut ReceiveQuarantine>,
994 unconnected: &HashMap<ObjectId, Unconnected>,
995 shallow: &ShallowPlan,
996 mut sideband_out: Option<&mut dyn Write>,
997 hook_records: &mut Vec<HookRunRecord>,
998) -> Result<Vec<RefUpdateResult>> {
999 let lookup_odb = quarantine.as_ref().map(|q| q.odb());
1000 let head_branch = ref_update_policy::head_branch(repo);
1001 let checked_out = ref_update_policy::checked_out_branch(repo);
1002 let branches = ref_update_policy::HeadBranches {
1003 head: head_branch.as_deref(),
1004 checked_out: checked_out.as_deref(),
1005 };
1006 let mut results: Vec<RefUpdateResult> = request
1007 .commands
1008 .iter()
1009 .zip(&shallow.roots)
1010 .map(|(c, roots)| {
1011 if !roots.is_empty() && !policy.shallow_update {
1012 return Ok(c.refused(RefUpdateRefusal::ShallowUpdateNotAllowed));
1013 }
1014 Ok(
1015 match ref_update_policy::validate_ref_update(
1016 repo,
1017 lookup_odb.as_ref(),
1018 c,
1019 policy,
1020 &opts.hidden_refs,
1021 branches,
1022 )? {
1023 Some(reason) => c.refused(reason),
1024 None => match c.new.and_then(|new| unconnected.get(&new)) {
1025 Some(gap) => c.refused(RefUpdateRefusal::Unconnected {
1026 missing: gap.oid,
1027 referrer: gap.referrer,
1028 }),
1029 None => c.accepted(),
1030 },
1031 },
1032 )
1033 })
1034 .collect::<Result<_>>()?;
1035
1036 let atomic = request.caps.contains("atomic");
1037 if atomic && results.iter().any(|r| r.error.is_some()) {
1038 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1039 return Ok(results);
1040 }
1041
1042 let accepted: Vec<RefCommand> = request
1043 .commands
1044 .iter()
1045 .zip(&results)
1046 .filter(|(_, r)| r.error.is_none())
1047 .map(|(c, _)| c.clone())
1048 .collect();
1049 let hook_ctx = ReceiveHookContext {
1050 push_options: &request.push_options,
1051 };
1052 if !accepted.is_empty() {
1053 if let Err(refusal) = hook.pre_receive_ctx(
1054 &accepted,
1055 lookup_odb.as_ref().unwrap_or(repo.odb()),
1056 &hook_ctx,
1057 ) {
1058 refuse_remaining(&mut results, &refusal);
1059 return Ok(results);
1060 }
1061 }
1062
1063 let scripts = ReceiveHookScripts::new(repo, &request.push_options, shallow.hook_env()?);
1064 let all_cmds: Vec<&RefCommand> = request.commands.iter().collect();
1065 let pre_result = scripts.pre_receive(&all_cmds, quarantine.as_deref());
1066 relay_hook_sideband(&mut sideband_out, &pre_result)?;
1067 hook_records.push(hook_record("pre-receive", &pre_result));
1068 if !pre_result.is_ok() {
1069 refuse_remaining(&mut results, &RefUpdateRefusal::PreReceiveHookDeclined);
1070 return Ok(results);
1071 }
1072
1073 if results.iter().all(|r| r.error.is_some()) {
1074 return Ok(results);
1075 }
1076
1077 let mut migrated_pack_keeps = Vec::new();
1078 if let Some(q) = quarantine {
1079 migrated_pack_keeps = q.migrate().map_err(ServeError::Repository)?;
1080 repo.odb().invalidate_packs();
1081 }
1082
1083 for (cmd, result) in request.commands.iter().zip(results.iter_mut()) {
1084 if result.error.is_some() {
1085 continue;
1086 }
1087 if let Err(refusal) = hook.update_ctx(cmd, repo.odb(), &hook_ctx) {
1088 result.error = Some(refusal);
1089 continue;
1090 }
1091 let update_result = scripts.update(cmd);
1092 relay_hook_sideband(&mut sideband_out, &update_result)?;
1093 hook_records.push(hook_record("update", &update_result));
1094 if !update_result.is_ok() {
1095 result.error = Some(RefUpdateRefusal::UpdateHookDeclined);
1096 }
1097 }
1098
1099 if atomic && results.iter().any(|r| r.error.is_some()) {
1100 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1101 return Ok(results);
1102 }
1103
1104 let new_roots: HashSet<ObjectId> = shallow
1105 .roots
1106 .iter()
1107 .zip(&results)
1108 .filter(|(_, r)| r.error.is_none())
1109 .flat_map(|(roots, _)| roots.iter().copied())
1110 .collect();
1111 if let Err(e) = add_shallow_boundaries(repo.git_dir(), &new_roots) {
1112 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1113 for (roots, result) in shallow.roots.iter().zip(results.iter_mut()) {
1114 if !roots.is_empty() && result.error.is_none() {
1115 result.error = Some(refusal.clone());
1116 }
1117 }
1118 if atomic {
1119 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1120 return Ok(results);
1121 }
1122 }
1123
1124 let pending: Vec<(usize, crate::refs::store::RefUpdate)> = request
1125 .commands
1126 .iter()
1127 .zip(results.iter())
1128 .enumerate()
1129 .filter(|(_, (_, r))| r.error.is_none())
1130 .map(|(idx, (cmd, _))| (idx, receive_command_store_update(cmd)))
1131 .collect();
1132
1133 let store = repo.refs();
1134 let ref_lock_wait = store.ref_lock_wait();
1135 if atomic {
1136 let batch: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1137 if let Err(e) =
1138 crate::refs::commit_ref_updates_on_store_with_wait(store, &batch, || ref_lock_wait)
1139 {
1140 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1141 for (idx, _) in pending {
1142 if results[idx].error.is_none() {
1143 results[idx].error = Some(refusal.clone());
1144 }
1145 }
1146 }
1147 } else {
1148 for (idx, message) in commit_receive_ref_updates_on_store(store, pending, ref_lock_wait) {
1149 results[idx].error = Some(RefUpdateRefusal::RefStore(message));
1150 }
1151 }
1152
1153 let successful: Vec<&RefCommand> = request
1154 .commands
1155 .iter()
1156 .zip(results.iter())
1157 .filter(|(_, r)| r.error.is_none())
1158 .map(|(c, _)| c)
1159 .collect();
1160 if !successful.is_empty() {
1161 let post_result = scripts.post_receive(&successful);
1162 relay_hook_sideband(&mut sideband_out, &post_result)?;
1163 hook_records.push(hook_record("post-receive", &post_result));
1164
1165 let pu_result = scripts.post_update(&successful);
1166 relay_hook_sideband(&mut sideband_out, &pu_result)?;
1167 hook_records.push(hook_record("post-update", &pu_result));
1168 }
1169
1170 drop(migrated_pack_keeps);
1171
1172 Ok(results)
1173}
1174
1175fn refuse_remaining(results: &mut [RefUpdateResult], reason: &RefUpdateRefusal) {
1177 for r in results.iter_mut().filter(|r| r.error.is_none()) {
1178 r.error = Some(reason.clone());
1179 }
1180}
1181
1182fn hook_record(name: &str, result: &HookResult) -> HookRunRecord {
1183 HookRunRecord {
1184 name: name.to_owned(),
1185 ran: result.was_executed(),
1186 exit_code: result.status(),
1187 }
1188}
1189
1190pub(crate) struct ReceiveHookScripts<'a> {
1199 repo: &'a Repository,
1200 receive_env: Vec<(String, String)>,
1202 update_env: Vec<(String, String)>,
1204}
1205
1206impl<'a> ReceiveHookScripts<'a> {
1207 pub(crate) fn new(
1210 repo: &'a Repository,
1211 push_options: &[String],
1212 extra_env: Vec<(String, String)>,
1213 ) -> Self {
1214 let mut receive_env = Vec::with_capacity(push_options.len() + 1 + extra_env.len());
1215 receive_env.push((
1216 "GIT_PUSH_OPTION_COUNT".to_owned(),
1217 push_options.len().to_string(),
1218 ));
1219 receive_env.extend(
1220 push_options
1221 .iter()
1222 .enumerate()
1223 .map(|(i, opt)| (format!("GIT_PUSH_OPTION_{i}"), opt.clone())),
1224 );
1225 receive_env.extend(extra_env.iter().cloned());
1226 Self {
1227 repo,
1228 receive_env,
1229 update_env: extra_env,
1230 }
1231 }
1232
1233 pub(crate) fn pre_receive(
1238 &self,
1239 commands: &[&RefCommand],
1240 quarantine: Option<&ReceiveQuarantine>,
1241 ) -> HookResult {
1242 let mut env = self.receive_env.clone();
1243 if let Some(q) = quarantine {
1244 env.extend(q.hook_env());
1245 }
1246 self.run_with_commands("pre-receive", commands, &env)
1247 }
1248
1249 pub(crate) fn update(&self, cmd: &RefCommand) -> HookResult {
1251 let algo = self.repo.odb().hash_algo();
1252 let hex = |oid: Option<ObjectId>| oid.unwrap_or_else(|| ObjectId::null(algo)).to_hex();
1253 let (old, new) = (hex(cmd.old), hex(cmd.new));
1254 let args = [cmd.refname.as_str(), old.as_str(), new.as_str()];
1255 run_hook_in_git_dir(self.repo, "update", &args, None, &pairs(&self.update_env))
1256 }
1257
1258 pub(crate) fn post_receive(&self, commands: &[&RefCommand]) -> HookResult {
1260 self.run_with_commands("post-receive", commands, &self.receive_env)
1261 }
1262
1263 pub(crate) fn post_update(&self, commands: &[&RefCommand]) -> HookResult {
1265 let args: Vec<&str> = commands.iter().map(|c| c.refname.as_str()).collect();
1266 run_hook_in_git_dir(
1267 self.repo,
1268 "post-update",
1269 &args,
1270 None,
1271 &pairs(&self.update_env),
1272 )
1273 }
1274
1275 fn run_with_commands(
1276 &self,
1277 hook_name: &str,
1278 commands: &[&RefCommand],
1279 env: &[(String, String)],
1280 ) -> HookResult {
1281 let zero = ObjectId::null(self.repo.odb().hash_algo());
1282 let stdin: String = commands
1283 .iter()
1284 .map(|c| {
1285 format!(
1286 "{} {} {}\n",
1287 c.old.unwrap_or(zero).to_hex(),
1288 c.new.unwrap_or(zero).to_hex(),
1289 c.refname
1290 )
1291 })
1292 .collect();
1293 run_hook_in_git_dir(
1294 self.repo,
1295 hook_name,
1296 &[],
1297 Some(stdin.as_bytes()),
1298 &pairs(env),
1299 )
1300 }
1301}
1302
1303fn pairs(env: &[(String, String)]) -> Vec<(&str, &str)> {
1304 env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
1305}
1306
1307fn receive_updates_eligible_for_initial_packed(updates: &[crate::refs::store::RefUpdate]) -> bool {
1308 use crate::refs::store::{Expected, RawRef};
1309 updates.iter().all(|update| {
1310 matches!(update.new_value, Some(RawRef::Direct(_)))
1311 && matches!(update.expected, Expected::Missing)
1312 && !update.flags.log_only
1313 && update.reflog.is_none()
1314 })
1315}
1316
1317fn commit_receive_ref_updates_on_store(
1323 store: &dyn crate::refs::store::RefStore,
1324 pending: Vec<(usize, crate::refs::store::RefUpdate)>,
1325 ref_lock_wait: crate::lockfile::LockWait,
1326) -> Vec<(usize, String)> {
1327 if pending.is_empty() {
1328 return Vec::new();
1329 }
1330 let updates: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1331 let batch_ok = if receive_updates_eligible_for_initial_packed(&updates) {
1332 crate::refs::commit_initial_ref_updates_on_store_with_wait(store, updates.clone(), || {
1333 ref_lock_wait
1334 })
1335 } else {
1336 crate::refs::commit_ref_updates_on_store_with_wait(store, &updates, || ref_lock_wait)
1337 };
1338 if batch_ok.is_ok() {
1339 return Vec::new();
1340 }
1341 let indices: Vec<_> = pending.iter().map(|(idx, _)| *idx).collect();
1342 match crate::refs::commit_ref_updates_non_atomic_on_store(store, updates) {
1343 Ok(outcomes) => indices
1344 .into_iter()
1345 .zip(outcomes)
1346 .filter_map(|(idx, outcome)| outcome.result.err().map(|e| (idx, e.to_string())))
1347 .collect(),
1348 Err(e) => indices
1349 .into_iter()
1350 .map(|idx| (idx, e.to_string()))
1351 .collect(),
1352 }
1353}
1354
1355fn receive_command_store_update(cmd: &RefCommand) -> crate::refs::store::RefUpdate {
1356 let mut flags = crate::refs::store::RefUpdateFlags::default();
1357 if cmd.refname == "HEAD" {
1358 flags.no_deref = true;
1359 }
1360 crate::refs::store::RefUpdate {
1361 name: cmd.refname.clone(),
1362 new_value: cmd.new.map(crate::refs::store::RawRef::Direct),
1363 expected: match cmd.old {
1364 Some(oid) => crate::refs::store::Expected::Oid(oid),
1365 None => crate::refs::store::Expected::Missing,
1366 },
1367 reflog: None,
1368 flags,
1369 }
1370}
1371
1372fn relay_hook_sideband(out: &mut Option<&mut dyn Write>, result: &HookResult) -> Result<()> {
1374 let (Some(w), Some(output)) = (out.as_mut(), result.output()) else {
1375 return Ok(());
1376 };
1377 for chunk in output
1378 .stdout
1379 .chunks(65515)
1380 .chain(output.stderr.chunks(65515))
1381 {
1382 crate::pkt_line::write_sideband_packet(*w, 2, chunk).map_err(ServeError::Io)?;
1383 }
1384 Ok(())
1385}
1386
1387fn build_report(
1390 unpack_error: Option<&UnpackError>,
1391 updates: &[RefUpdateResult],
1392) -> Result<Vec<u8>> {
1393 let mut report = Vec::new();
1394 match unpack_error {
1395 None => write_line(&mut report, "unpack ok")?,
1396 Some(e) => write_line(&mut report, &format!("unpack {}", one_line(&e.to_string())))?,
1397 }
1398 for u in updates {
1399 match &u.error {
1400 None => write_line(&mut report, &format!("ok {}", u.refname))?,
1401 Some(reason) => write_line(
1402 &mut report,
1403 &format!("ng {} {}", u.refname, one_line(&reason.to_string())),
1404 )?,
1405 }
1406 }
1407 write_flush(&mut report)?;
1408 Ok(report)
1409}
1410
1411fn one_line(msg: &str) -> String {
1413 msg.lines().next().unwrap_or_default().trim().to_owned()
1414}
1415
1416struct SpooledPack {
1418 file: tempfile::NamedTempFile,
1419 header: [u8; 12],
1421}
1422
1423fn spool_push_pack(
1431 input: &mut dyn Read,
1432 odb: &Odb,
1433 max_bytes: Option<u64>,
1434 stop: &CancelSignal<'_>,
1435) -> std::result::Result<SpooledPack, UnpackError> {
1436 let hash = odb.hash_algo();
1437 let mut file = tempfile::Builder::new()
1438 .prefix("tmp_pack_")
1439 .tempfile_in(odb.objects_dir().join("pack"))?;
1440 let mut scanner = PackStreamScanner::new(hash);
1441 let mut header = [0u8; 12];
1442 let mut chunk = vec![0u8; SPOOL_CHUNK];
1443 let mut written = 0u64;
1444 let mut pack_len: Option<u64> = None;
1445 let mut out = BufWriter::with_capacity(SPOOL_CHUNK, file.as_file_mut());
1446 loop {
1447 stop.poll().map_err(|_| UnpackError::Cancelled)?;
1448 let want = match pack_len {
1449 Some(len) if written >= len => break,
1450 Some(len) => usize::try_from(len - written).map_or(chunk.len(), |n| n.min(chunk.len())),
1451 None => chunk.len(),
1452 };
1453 let n = read_some(input, &mut chunk[..want])?;
1454 if n == 0 {
1455 return Err(UnpackError::Truncated);
1456 }
1457 let mut data = &chunk[..n];
1458 if pack_len.is_none() {
1459 scanner.feed(data)?;
1460 if scanner.is_complete() {
1461 let len = scanner
1462 .pack_body_byte_len()
1463 .saturating_add(hash.len() as u64);
1464 let keep = usize::try_from(len.saturating_sub(written)).map_or(n, |k| k.min(n));
1465 data = &data[..keep];
1466 pack_len = Some(len);
1467 }
1468 }
1469 if let Some(limit) = max_bytes {
1470 let total = pack_len.unwrap_or(written.saturating_add(data.len() as u64));
1471 if total > limit {
1472 return Err(UnpackError::TooLarge { limit });
1473 }
1474 }
1475 if let Some(missing) = usize::try_from(written)
1476 .ok()
1477 .and_then(|start| header.get_mut(start..))
1478 {
1479 let take = missing.len().min(data.len());
1480 missing[..take].copy_from_slice(&data[..take]);
1481 }
1482 out.write_all(data)?;
1483 written += data.len() as u64;
1484 }
1485 out.flush()?;
1486 drop(out);
1487 Ok(SpooledPack { file, header })
1488}
1489
1490fn read_some(input: &mut dyn Read, buf: &mut [u8]) -> std::io::Result<usize> {
1492 loop {
1493 match input.read(buf) {
1494 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1495 other => return other,
1496 }
1497 }
1498}
1499
1500fn check_received_objects(
1505 repo: &Repository,
1506 odb: &Odb,
1507 indexed: Option<&HashSet<ObjectId>>,
1508 cfg: &ConfigSet,
1509 fsck: &ObjectFsckPolicy,
1510) -> std::result::Result<(), UnpackError> {
1511 let checker = fsck.checker(odb.hash_algo(), repo.git_dir(), repo.environment())?;
1512 let mut received: Vec<ObjectId> = match indexed {
1513 Some(ids) => ids.iter().copied().collect(),
1514 None => odb
1515 .enumerate_local_loose_objects()?
1516 .into_iter()
1517 .map(|(oid, _)| oid)
1518 .collect(),
1519 };
1520 received.sort_unstable();
1521 let parallel = repo
1522 .thread_policy()
1523 .try_resolve(ParallelOp::IndexPack, cfg)
1524 .map_err(crate::error::Error::from)?;
1525 let verdicts = try_par_map(&received, parallel, received.len(), |oid| {
1526 if odb.read_info(oid)?.kind == ObjectKind::Blob {
1527 return Ok(None);
1528 }
1529 let obj = odb.read(oid)?;
1530 Ok(checker
1531 .check(oid, obj.kind, &obj.data)
1532 .err()
1533 .map(|error| RejectedObject {
1534 oid: *oid,
1535 kind: obj.kind,
1536 error,
1537 }))
1538 })
1539 .map_err(|ParallelHashError::Task(e): ParallelHashError<crate::error::Error>| e)?;
1540 match verdicts.into_iter().flatten().next() {
1541 Some(rejected) => Err(UnpackError::Rejected(rejected)),
1542 None => Ok(()),
1543 }
1544}
1545
1546fn unconnected_tips(
1552 repo: &Repository,
1553 odb: &Odb,
1554 received: &HashSet<ObjectId>,
1555 existing: &HashSet<ObjectId>,
1556 commands: &[RefCommand],
1557 shallow_boundaries: &HashSet<ObjectId>,
1558 promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1559) -> std::result::Result<HashMap<ObjectId, Unconnected>, UnpackError> {
1560 let mut tips: Vec<ObjectId> = commands.iter().filter_map(|c| c.new).collect();
1561 tips.sort_unstable();
1562 tips.dedup();
1563 if tips.is_empty() {
1564 return Ok(HashMap::new());
1565 }
1566 let receiver_grafts = load_shallow_boundaries(repo.git_dir());
1567 let graph = repo.odb().commit_graph_chain();
1568 let pushed = PushedObjects {
1569 odb,
1570 repo_odb: repo.odb(),
1571 graph: graph.as_deref(),
1572 received,
1573 existing,
1574 shallow_boundaries,
1575 receiver_grafts: &receiver_grafts,
1576 promises,
1577 };
1578 let gaps = pushed.unconnected_tips(&tips)?;
1579 Ok(tips
1580 .into_iter()
1581 .zip(gaps)
1582 .filter_map(|(tip, gap)| Some((tip, gap?)))
1583 .collect())
1584}
1585
1586fn existing_ref_values(repo: &Repository) -> Result<HashSet<ObjectId>> {
1588 let mut existing = HashSet::new();
1589 for r in advertised_refs(repo, &[], &[], Peel::Yes)? {
1590 existing.insert(r.oid);
1591 existing.extend(r.peeled);
1592 }
1593 existing.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
1594 Ok(existing)
1595}
1596
1597fn ingest_spooled_pack(
1609 odb: &Odb,
1610 pack: SpooledPack,
1611 cfg: &ConfigSet,
1612 shallow_boundaries: &HashSet<ObjectId>,
1613 promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1614) -> std::result::Result<Option<HashSet<ObjectId>>, UnpackError> {
1615 if should_use_unpack_objects(&pack.header, cfg) {
1616 let mut reader = BufReader::with_capacity(SPOOL_CHUNK, pack.file.reopen()?);
1617 let opts = UnpackOptions {
1618 quiet: true,
1619 strict: true,
1620 shallow_boundaries: shallow_boundaries.clone(),
1621 ..UnpackOptions::default()
1622 };
1623 match promises {
1624 Some(promises) => {
1625 let promised =
1626 |oid: &ObjectId, referrer: &ObjectId| promises.is_promised(oid, Some(referrer));
1627 crate::unpack_objects::unpack_objects_with_promises(
1628 &mut reader,
1629 odb,
1630 &opts,
1631 &promised,
1632 )?;
1633 }
1634 None => {
1635 unpack_objects(&mut reader, odb, &opts)?;
1636 }
1637 }
1638 return Ok(None);
1639 }
1640 let path = pack.file.into_temp_path();
1641 let installed = install_pack_path(
1642 &path,
1643 odb,
1644 &IngestPackOptions {
1645 fix_thin: true,
1646 skip_post_index_verify: true,
1649 merged_config: Some(cfg.clone()),
1650 ..Default::default()
1651 },
1652 )?;
1653 Ok(Some(installed.object_ids))
1654}
1655
1656#[cfg(test)]
1657mod tests {
1658 use super::*;
1659
1660 #[test]
1661 fn report_lists_unpack_status_then_each_ref() {
1662 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
1663 .unwrap_or_else(|_| ObjectId::zero());
1664 let updates = vec![
1665 RefUpdateResult {
1666 refname: "refs/heads/main".into(),
1667 old: None,
1668 new: Some(oid),
1669 error: None,
1670 },
1671 RefUpdateResult {
1672 refname: "refs/heads/topic".into(),
1673 old: Some(oid),
1674 new: None,
1675 error: Some(RefUpdateRefusal::DeletionProhibited),
1676 },
1677 ];
1678 let report = build_report(None, &updates).unwrap_or_default();
1679 assert_eq!(
1680 String::from_utf8_lossy(&report),
1681 "000eunpack ok\n0017ok refs/heads/main\n002cng refs/heads/topic deletion prohibited\n0000"
1682 );
1683 }
1684
1685 #[test]
1686 fn report_renders_typed_unpack_error_and_refusal() {
1687 let updates = vec![RefUpdateResult {
1688 refname: "refs/heads/main".into(),
1689 old: None,
1690 new: None,
1691 error: Some(RefUpdateRefusal::UnpackerError),
1692 }];
1693 let report = build_report(Some(&UnpackError::Truncated), &updates).unwrap_or_default();
1694 assert_eq!(
1695 String::from_utf8_lossy(&report),
1696 "0029unpack unexpected end of pack stream\n0026ng refs/heads/main unpacker error\n0000"
1697 );
1698 }
1699
1700 #[test]
1701 fn refusals_render_the_reasons_git_clients_expect() {
1702 let cases = [
1703 (RefUpdateRefusal::InvalidRefName, "funny refname"),
1704 (RefUpdateRefusal::HiddenRef, "deny updating a hidden ref"),
1705 (RefUpdateRefusal::StaleInfo, "stale info"),
1706 (RefUpdateRefusal::DeletionProhibited, "deletion prohibited"),
1707 (
1708 RefUpdateRefusal::CurrentBranchDeletion,
1709 "deletion of the current branch prohibited",
1710 ),
1711 (
1712 RefUpdateRefusal::CurrentBranch,
1713 "branch is currently checked out",
1714 ),
1715 (
1716 RefUpdateRefusal::MissingObjects,
1717 "missing necessary objects",
1718 ),
1719 (
1720 RefUpdateRefusal::Unconnected {
1721 missing: ObjectId::zero(),
1722 referrer: ObjectId::zero(),
1723 },
1724 "missing necessary objects",
1725 ),
1726 (RefUpdateRefusal::NonFastForward, "non-fast-forward"),
1727 (RefUpdateRefusal::BadRef, "bad ref"),
1728 (
1729 RefUpdateRefusal::FastForwardCheckFailed,
1730 "unable to verify fast-forward",
1731 ),
1732 (
1733 RefUpdateRefusal::ShallowUpdateNotAllowed,
1734 "shallow update not allowed",
1735 ),
1736 (RefUpdateRefusal::UnpackerError, "unpacker error"),
1737 (
1738 RefUpdateRefusal::AtomicTransactionFailed,
1739 "atomic transaction failed",
1740 ),
1741 (
1742 RefUpdateRefusal::PreReceiveHookDeclined,
1743 "pre-receive hook declined",
1744 ),
1745 (RefUpdateRefusal::UpdateHookDeclined, "hook declined"),
1746 (RefUpdateRefusal::Policy("frozen".into()), "frozen"),
1747 (
1748 RefUpdateRefusal::TargetBranch {
1749 target: "main".into(),
1750 },
1751 "main is the target branch; it changes only through integrate",
1752 ),
1753 (
1754 RefUpdateRefusal::RefStore("locked".into()),
1755 "failed to update ref: locked",
1756 ),
1757 ];
1758 for (refusal, wire) in cases {
1759 assert_eq!(refusal.to_string(), wire);
1760 }
1761 }
1762
1763 struct NoReadPastEnd<'a> {
1766 data: &'a [u8],
1767 }
1768
1769 impl Read for NoReadPastEnd<'_> {
1770 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1771 if self.data.is_empty() {
1772 return Err(std::io::Error::other("read past the end of the pack"));
1773 }
1774 let n = buf.len().min(self.data.len());
1775 buf[..n].copy_from_slice(&self.data[..n]);
1776 self.data = &self.data[n..];
1777 Ok(n)
1778 }
1779 }
1780
1781 fn sample_pack() -> Vec<u8> {
1782 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
1783 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
1784 let mut seed = 0x2545_f491_u32;
1785 let big: Vec<u8> = (0..(SPOOL_CHUNK * 3))
1787 .map(|_| {
1788 seed ^= seed << 13;
1789 seed ^= seed >> 17;
1790 seed ^= seed << 5;
1791 seed as u8
1792 })
1793 .collect();
1794 pb.add_full(PK::Blob, &big);
1795 pb.add_full(PK::Blob, b"small\n");
1796 pb.build().bytes
1797 }
1798
1799 fn test_odb(dir: &std::path::Path) -> Odb {
1800 let objects = dir.join("objects");
1801 std::fs::create_dir_all(objects.join("pack")).unwrap_or_default();
1802 Odb::new(&objects)
1803 }
1804
1805 #[test]
1806 fn spool_stops_at_the_trailer_without_reading_further() {
1807 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1808 let odb = test_odb(tmp.path());
1809 let pack = sample_pack();
1810 let mut input = NoReadPastEnd { data: &pack };
1811 let spooled = spool_push_pack(&mut input, &odb, None, &CancelSignal::never())
1812 .unwrap_or_else(|e| panic!("{e}"));
1813 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1814 assert_eq!(&spooled.header[..], &pack[..12]);
1815 }
1816
1817 #[test]
1818 fn spool_keeps_only_the_pack_when_more_bytes_follow() {
1819 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1820 let odb = test_odb(tmp.path());
1821 let pack = sample_pack();
1822 let mut stream = pack.clone();
1823 stream.extend_from_slice(b"trailing junk");
1824 let spooled = spool_push_pack(&mut stream.as_slice(), &odb, None, &CancelSignal::never())
1825 .unwrap_or_else(|e| panic!("{e}"));
1826 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1827 }
1828
1829 #[test]
1830 fn spool_reports_typed_errors() {
1831 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1832 let odb = test_odb(tmp.path());
1833 let pack = sample_pack();
1834
1835 let truncated = &pack[..pack.len() - 5];
1836 let never = CancelSignal::never();
1837 assert!(matches!(
1838 spool_push_pack(&mut &truncated[..], &odb, None, &never),
1839 Err(UnpackError::Truncated)
1840 ));
1841
1842 let limit = (pack.len() - 1) as u64;
1843 assert!(matches!(
1844 spool_push_pack(&mut pack.as_slice(), &odb, Some(limit), &never),
1845 Err(UnpackError::TooLarge { limit: l }) if l == limit
1846 ));
1847 assert!(
1848 spool_push_pack(&mut pack.as_slice(), &odb, Some(pack.len() as u64), &never).is_ok()
1849 );
1850
1851 assert!(matches!(
1852 spool_push_pack(&mut &b"JUNK\0\0\0\x02\0\0\0\x01"[..], &odb, None, &never),
1853 Err(UnpackError::Malformed(PackStreamScanError::BadSignature))
1854 ));
1855
1856 let leftovers: Vec<_> = std::fs::read_dir(odb.objects_dir().join("pack"))
1857 .map(|rd| rd.filter_map(|e| e.ok()).collect())
1858 .unwrap_or_default();
1859 assert!(
1860 leftovers.is_empty(),
1861 "failed spools must not leave temp files"
1862 );
1863 }
1864
1865 #[test]
1866 fn deny_current_branch_values() {
1867 let policy_for = |value: Option<&str>| {
1868 let mut config = ConfigSet::new();
1869 if let Some(v) = value {
1870 let _ = config.add_command_override("receive.denyCurrentBranch", v);
1871 }
1872 ReceivePolicy::from_config(&config).deny_current_branch
1873 };
1874 assert!(policy_for(None));
1875 assert!(!policy_for(Some("ignore")));
1876 assert!(!policy_for(Some("updateInstead")));
1877 assert!(policy_for(Some("refuse")));
1878 }
1879
1880 #[test]
1881 fn shallow_update_is_off_unless_configured() {
1882 assert!(!ReceivePolicy::default().shallow_update);
1883 let mut config = ConfigSet::new();
1884 let _ = config.add_command_override("receive.shallowUpdate", "true");
1885 assert!(ReceivePolicy::from_config(&config).shallow_update);
1886 }
1887
1888 #[test]
1889 fn deny_delete_current_values() {
1890 let policy_for = |value: Option<&str>| {
1891 let mut config = ConfigSet::new();
1892 if let Some(v) = value {
1893 let _ = config.add_command_override("receive.denyDeleteCurrent", v);
1894 }
1895 ReceivePolicy::from_config(&config).deny_delete_current
1896 };
1897 assert!(policy_for(None));
1898 assert!(!policy_for(Some("ignore")));
1899 assert!(!policy_for(Some("warn")));
1900 assert!(!policy_for(Some("false")));
1901 assert!(policy_for(Some("updateInstead")));
1903 assert!(policy_for(Some("refuse")));
1904 assert!(ReceivePolicy::default().deny_delete_current);
1905 }
1906}