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
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 mut first_line = true;
781 for_each_advertised_ref(repo, &opts.hidden_refs, &[], Peel::No, |r| {
782 if first_line {
783 write_line(output, &format!("{} {}\0{caps}", r.oid.to_hex(), r.name))?;
784 first_line = false;
785 } else {
786 write_line(output, &format!("{} {}", r.oid.to_hex(), r.name))?;
787 }
788 Ok(std::ops::ControlFlow::Continue(()))
789 })?;
790 if first_line {
791 let zero = ObjectId::null(repo.odb().hash_algo());
792 write_line(
793 output,
794 &format!("{} capabilities^{{}}\0{caps}", zero.to_hex()),
795 )?;
796 }
797 write_flush(output)
798}
799
800struct PushRequest {
802 commands: Vec<RefCommand>,
803 caps: HashSet<String>,
804 push_options: Vec<String>,
805 shallow: Vec<ObjectId>,
807}
808
809fn read_commands(repo: &Repository, input: &mut dyn Read) -> Result<Option<PushRequest>> {
815 let mut commands = Vec::new();
816 let mut caps = HashSet::new();
817 let mut shallow = Vec::new();
818 loop {
819 match read_packet(input)? {
820 None | Some(Packet::Flush) => break,
821 Some(Packet::Data(line)) => {
822 if let Some(hex) = line.strip_prefix("shallow ") {
823 shallow.push(parse_oid(repo, hex)?);
824 continue;
825 }
826 let (line, line_caps) = line.split_once('\0').unwrap_or((line.as_str(), ""));
827 if commands.is_empty() {
828 caps.extend(
829 line_caps
830 .split(' ')
831 .filter(|c| !c.is_empty())
832 .map(str::to_owned),
833 );
834 }
835 let mut parts = line.splitn(3, ' ');
836 let (Some(old), Some(new), Some(refname)) =
837 (parts.next(), parts.next(), parts.next())
838 else {
839 return Err(ServeError::Protocol(format!("malformed command '{line}'")));
840 };
841 let old = parse_oid(repo, old)?;
842 let new = parse_oid(repo, new)?;
843 commands.push(RefCommand {
844 old: (!old.is_zero()).then_some(old),
845 new: (!new.is_zero()).then_some(new),
846 refname: refname.to_owned(),
847 });
848 }
849 Some(other) => {
850 return Err(ServeError::Protocol(format!(
851 "unexpected packet in command list: {other:?}"
852 )))
853 }
854 }
855 }
856 if commands.is_empty() {
857 return Ok(None);
858 }
859 let mut push_options = Vec::new();
860 if caps.contains("push-options") {
861 loop {
862 match read_packet(input)? {
863 None | Some(Packet::Flush) => break,
864 Some(Packet::Data(line)) => {
865 push_options.push(line);
866 }
867 Some(other) => {
868 return Err(ServeError::Protocol(format!(
869 "unexpected packet in push options: {other:?}"
870 )))
871 }
872 }
873 }
874 }
875 let format = caps.iter().find_map(|c| c.strip_prefix("object-format="));
876 check_object_format(repo, format)?;
877 Ok(Some(PushRequest {
878 commands,
879 caps,
880 push_options,
881 shallow,
882 }))
883}
884
885struct ShallowPlan {
887 roots: Vec<Vec<ObjectId>>,
890 hook_file: Option<tempfile::NamedTempFile>,
894}
895
896impl ShallowPlan {
897 fn none(commands: usize) -> Self {
899 Self {
900 roots: vec![Vec::new(); commands],
901 hook_file: None,
902 }
903 }
904
905 fn new(
908 repo: &Repository,
909 odb: &Odb,
910 request: &PushRequest,
911 policy: &ReceivePolicy,
912 info: &PushShallowInfo,
913 receiver_grafts: &HashSet<ObjectId>,
914 ) -> Result<Self> {
915 if info.is_empty() {
916 return Ok(Self::none(request.commands.len()));
917 }
918 let mut receiver_tips: HashSet<ObjectId> = HashSet::new();
919 super::for_each_advertised_ref(repo, &[], &[], Peel::Yes, |r| {
920 receiver_tips.insert(r.peeled.unwrap_or(r.oid));
921 Ok(std::ops::ControlFlow::Continue(()))
922 })?;
923 receiver_tips.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
924 let graph = repo.odb().commit_graph_chain();
925 let roots = request
926 .commands
927 .iter()
928 .map(|c| match c.new {
929 Some(new) => {
930 info.new_roots(odb, graph.as_deref(), new, receiver_grafts, &receiver_tips)
931 }
932 None => Ok(Vec::new()),
933 })
934 .collect::<crate::error::Result<_>>()?;
935 let hook_file = if policy.shallow_update {
936 Some(temporary_shallow_file(
937 repo,
938 receiver_grafts,
939 &request.shallow,
940 )?)
941 } else {
942 None
943 };
944 Ok(Self { roots, hook_file })
945 }
946
947 fn hook_env(&self) -> Result<Vec<(String, String)>> {
949 let Some(file) = &self.hook_file else {
950 return Ok(Vec::new());
951 };
952 let path = std::path::absolute(file.path())?;
953 Ok(vec![(
954 "GIT_SHALLOW_FILE".to_owned(),
955 path.to_string_lossy().into_owned(),
956 )])
957 }
958}
959
960fn temporary_shallow_file(
963 repo: &Repository,
964 receiver_grafts: &HashSet<ObjectId>,
965 client: &[ObjectId],
966) -> Result<tempfile::NamedTempFile> {
967 let mut hexes: Vec<String> = receiver_grafts
968 .iter()
969 .chain(client)
970 .map(ObjectId::to_hex)
971 .collect();
972 hexes.sort_unstable();
973 hexes.dedup();
974 let dir = crate::shallow::shallow_file_path(repo.git_dir())
975 .parent()
976 .map(std::path::Path::to_path_buf)
977 .unwrap_or_else(|| repo.git_dir().to_path_buf());
978 let mut file = tempfile::Builder::new()
979 .prefix("shallow_")
980 .tempfile_in(dir)?;
981 for hex in hexes {
982 writeln!(file, "{hex}")?;
983 }
984 file.flush()?;
985 Ok(file)
986}
987
988#[allow(clippy::too_many_arguments)]
990fn apply_commands_with_hooks(
991 repo: &Repository,
992 request: &PushRequest,
993 opts: &ServeOptions,
994 policy: &ReceivePolicy,
995 hook: &dyn ReceiveHook,
996 quarantine: Option<&mut ReceiveQuarantine>,
997 unconnected: &HashMap<ObjectId, Unconnected>,
998 shallow: &ShallowPlan,
999 mut sideband_out: Option<&mut dyn Write>,
1000 hook_records: &mut Vec<HookRunRecord>,
1001) -> Result<Vec<RefUpdateResult>> {
1002 let lookup_odb = quarantine.as_ref().map(|q| q.odb());
1003 let head_branch = ref_update_policy::head_branch(repo);
1004 let checked_out = ref_update_policy::checked_out_branch(repo);
1005 let branches = ref_update_policy::HeadBranches {
1006 head: head_branch.as_deref(),
1007 checked_out: checked_out.as_deref(),
1008 };
1009 let mut results: Vec<RefUpdateResult> = request
1010 .commands
1011 .iter()
1012 .zip(&shallow.roots)
1013 .map(|(c, roots)| {
1014 if !roots.is_empty() && !policy.shallow_update {
1015 return Ok(c.refused(RefUpdateRefusal::ShallowUpdateNotAllowed));
1016 }
1017 Ok(
1018 match ref_update_policy::validate_ref_update(
1019 repo,
1020 lookup_odb.as_ref(),
1021 c,
1022 policy,
1023 &opts.hidden_refs,
1024 branches,
1025 )? {
1026 Some(reason) => c.refused(reason),
1027 None => match c.new.and_then(|new| unconnected.get(&new)) {
1028 Some(gap) => c.refused(RefUpdateRefusal::Unconnected {
1029 missing: gap.oid,
1030 referrer: gap.referrer,
1031 }),
1032 None => c.accepted(),
1033 },
1034 },
1035 )
1036 })
1037 .collect::<Result<_>>()?;
1038
1039 let atomic = request.caps.contains("atomic");
1040 if atomic && results.iter().any(|r| r.error.is_some()) {
1041 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1042 return Ok(results);
1043 }
1044
1045 let accepted: Vec<RefCommand> = request
1046 .commands
1047 .iter()
1048 .zip(&results)
1049 .filter(|(_, r)| r.error.is_none())
1050 .map(|(c, _)| c.clone())
1051 .collect();
1052 let hook_ctx = ReceiveHookContext {
1053 push_options: &request.push_options,
1054 };
1055 if !accepted.is_empty() {
1056 if let Err(refusal) = hook.pre_receive_ctx(
1057 &accepted,
1058 lookup_odb.as_ref().unwrap_or(repo.odb()),
1059 &hook_ctx,
1060 ) {
1061 refuse_remaining(&mut results, &refusal);
1062 return Ok(results);
1063 }
1064 }
1065
1066 let scripts = ReceiveHookScripts::new(repo, &request.push_options, shallow.hook_env()?);
1067 let all_cmds: Vec<&RefCommand> = request.commands.iter().collect();
1068 let pre_result = scripts.pre_receive(&all_cmds, quarantine.as_deref());
1069 relay_hook_sideband(&mut sideband_out, &pre_result)?;
1070 hook_records.push(hook_record("pre-receive", &pre_result));
1071 if !pre_result.is_ok() {
1072 refuse_remaining(&mut results, &RefUpdateRefusal::PreReceiveHookDeclined);
1073 return Ok(results);
1074 }
1075
1076 if results.iter().all(|r| r.error.is_some()) {
1077 return Ok(results);
1078 }
1079
1080 let mut migrated_pack_keeps = Vec::new();
1081 if let Some(q) = quarantine {
1082 migrated_pack_keeps = q.migrate().map_err(ServeError::Repository)?;
1083 repo.odb().invalidate_packs();
1084 }
1085
1086 for (cmd, result) in request.commands.iter().zip(results.iter_mut()) {
1087 if result.error.is_some() {
1088 continue;
1089 }
1090 if let Err(refusal) = hook.update_ctx(cmd, repo.odb(), &hook_ctx) {
1091 result.error = Some(refusal);
1092 continue;
1093 }
1094 let update_result = scripts.update(cmd);
1095 relay_hook_sideband(&mut sideband_out, &update_result)?;
1096 hook_records.push(hook_record("update", &update_result));
1097 if !update_result.is_ok() {
1098 result.error = Some(RefUpdateRefusal::UpdateHookDeclined);
1099 }
1100 }
1101
1102 if atomic && results.iter().any(|r| r.error.is_some()) {
1103 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1104 return Ok(results);
1105 }
1106
1107 let new_roots: HashSet<ObjectId> = shallow
1108 .roots
1109 .iter()
1110 .zip(&results)
1111 .filter(|(_, r)| r.error.is_none())
1112 .flat_map(|(roots, _)| roots.iter().copied())
1113 .collect();
1114 if let Err(e) = add_shallow_boundaries(repo.git_dir(), &new_roots) {
1115 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1116 for (roots, result) in shallow.roots.iter().zip(results.iter_mut()) {
1117 if !roots.is_empty() && result.error.is_none() {
1118 result.error = Some(refusal.clone());
1119 }
1120 }
1121 if atomic {
1122 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1123 return Ok(results);
1124 }
1125 }
1126
1127 let pending: Vec<(usize, crate::refs::store::RefUpdate)> = request
1128 .commands
1129 .iter()
1130 .zip(results.iter())
1131 .enumerate()
1132 .filter(|(_, (_, r))| r.error.is_none())
1133 .map(|(idx, (cmd, _))| (idx, receive_command_store_update(cmd)))
1134 .collect();
1135
1136 let store = repo.refs();
1137 let ref_lock_wait = store.ref_lock_wait();
1138 if atomic {
1139 let batch: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1140 if let Err(e) =
1141 crate::refs::commit_ref_updates_on_store_with_wait(store, &batch, || ref_lock_wait)
1142 {
1143 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1144 for (idx, _) in pending {
1145 if results[idx].error.is_none() {
1146 results[idx].error = Some(refusal.clone());
1147 }
1148 }
1149 }
1150 } else {
1151 for (idx, message) in commit_receive_ref_updates_on_store(store, pending, ref_lock_wait) {
1152 results[idx].error = Some(RefUpdateRefusal::RefStore(message));
1153 }
1154 }
1155
1156 let successful: Vec<&RefCommand> = request
1157 .commands
1158 .iter()
1159 .zip(results.iter())
1160 .filter(|(_, r)| r.error.is_none())
1161 .map(|(c, _)| c)
1162 .collect();
1163 if !successful.is_empty() {
1164 let post_result = scripts.post_receive(&successful);
1165 relay_hook_sideband(&mut sideband_out, &post_result)?;
1166 hook_records.push(hook_record("post-receive", &post_result));
1167
1168 let pu_result = scripts.post_update(&successful);
1169 relay_hook_sideband(&mut sideband_out, &pu_result)?;
1170 hook_records.push(hook_record("post-update", &pu_result));
1171 }
1172
1173 drop(migrated_pack_keeps);
1174
1175 Ok(results)
1176}
1177
1178fn refuse_remaining(results: &mut [RefUpdateResult], reason: &RefUpdateRefusal) {
1180 for r in results.iter_mut().filter(|r| r.error.is_none()) {
1181 r.error = Some(reason.clone());
1182 }
1183}
1184
1185fn hook_record(name: &str, result: &HookResult) -> HookRunRecord {
1186 HookRunRecord {
1187 name: name.to_owned(),
1188 ran: result.was_executed(),
1189 exit_code: result.status(),
1190 }
1191}
1192
1193pub(crate) struct ReceiveHookScripts<'a> {
1202 repo: &'a Repository,
1203 receive_env: Vec<(String, String)>,
1205 update_env: Vec<(String, String)>,
1207}
1208
1209impl<'a> ReceiveHookScripts<'a> {
1210 pub(crate) fn new(
1213 repo: &'a Repository,
1214 push_options: &[String],
1215 extra_env: Vec<(String, String)>,
1216 ) -> Self {
1217 let mut receive_env = Vec::with_capacity(push_options.len() + 1 + extra_env.len());
1218 receive_env.push((
1219 "GIT_PUSH_OPTION_COUNT".to_owned(),
1220 push_options.len().to_string(),
1221 ));
1222 receive_env.extend(
1223 push_options
1224 .iter()
1225 .enumerate()
1226 .map(|(i, opt)| (format!("GIT_PUSH_OPTION_{i}"), opt.clone())),
1227 );
1228 receive_env.extend(extra_env.iter().cloned());
1229 Self {
1230 repo,
1231 receive_env,
1232 update_env: extra_env,
1233 }
1234 }
1235
1236 pub(crate) fn pre_receive(
1241 &self,
1242 commands: &[&RefCommand],
1243 quarantine: Option<&ReceiveQuarantine>,
1244 ) -> HookResult {
1245 let mut env = self.receive_env.clone();
1246 if let Some(q) = quarantine {
1247 env.extend(q.hook_env());
1248 }
1249 self.run_with_commands("pre-receive", commands, &env)
1250 }
1251
1252 pub(crate) fn update(&self, cmd: &RefCommand) -> HookResult {
1254 let algo = self.repo.odb().hash_algo();
1255 let hex = |oid: Option<ObjectId>| oid.unwrap_or_else(|| ObjectId::null(algo)).to_hex();
1256 let (old, new) = (hex(cmd.old), hex(cmd.new));
1257 let args = [cmd.refname.as_str(), old.as_str(), new.as_str()];
1258 run_hook_in_git_dir(self.repo, "update", &args, None, &pairs(&self.update_env))
1259 }
1260
1261 pub(crate) fn post_receive(&self, commands: &[&RefCommand]) -> HookResult {
1263 self.run_with_commands("post-receive", commands, &self.receive_env)
1264 }
1265
1266 pub(crate) fn post_update(&self, commands: &[&RefCommand]) -> HookResult {
1268 let args: Vec<&str> = commands.iter().map(|c| c.refname.as_str()).collect();
1269 run_hook_in_git_dir(
1270 self.repo,
1271 "post-update",
1272 &args,
1273 None,
1274 &pairs(&self.update_env),
1275 )
1276 }
1277
1278 fn run_with_commands(
1279 &self,
1280 hook_name: &str,
1281 commands: &[&RefCommand],
1282 env: &[(String, String)],
1283 ) -> HookResult {
1284 let zero = ObjectId::null(self.repo.odb().hash_algo());
1285 let stdin: String = commands
1286 .iter()
1287 .map(|c| {
1288 format!(
1289 "{} {} {}\n",
1290 c.old.unwrap_or(zero).to_hex(),
1291 c.new.unwrap_or(zero).to_hex(),
1292 c.refname
1293 )
1294 })
1295 .collect();
1296 run_hook_in_git_dir(
1297 self.repo,
1298 hook_name,
1299 &[],
1300 Some(stdin.as_bytes()),
1301 &pairs(env),
1302 )
1303 }
1304}
1305
1306fn pairs(env: &[(String, String)]) -> Vec<(&str, &str)> {
1307 env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
1308}
1309
1310fn receive_updates_eligible_for_initial_packed(updates: &[crate::refs::store::RefUpdate]) -> bool {
1311 use crate::refs::store::{Expected, RawRef};
1312 updates.iter().all(|update| {
1313 matches!(update.new_value, Some(RawRef::Direct(_)))
1314 && matches!(update.expected, Expected::Missing)
1315 && !update.flags.log_only
1316 && update.reflog.is_none()
1317 })
1318}
1319
1320fn commit_receive_ref_updates_on_store(
1326 store: &dyn crate::refs::store::RefStore,
1327 pending: Vec<(usize, crate::refs::store::RefUpdate)>,
1328 ref_lock_wait: crate::lockfile::LockWait,
1329) -> Vec<(usize, String)> {
1330 if pending.is_empty() {
1331 return Vec::new();
1332 }
1333 let updates: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1334 let batch_ok = if receive_updates_eligible_for_initial_packed(&updates) {
1335 crate::refs::commit_initial_ref_updates_on_store_with_wait(store, updates.clone(), || {
1336 ref_lock_wait
1337 })
1338 } else {
1339 crate::refs::commit_ref_updates_on_store_with_wait(store, &updates, || ref_lock_wait)
1340 };
1341 if batch_ok.is_ok() {
1342 return Vec::new();
1343 }
1344 let indices: Vec<_> = pending.iter().map(|(idx, _)| *idx).collect();
1345 match crate::refs::commit_ref_updates_non_atomic_on_store(store, updates) {
1346 Ok(outcomes) => indices
1347 .into_iter()
1348 .zip(outcomes)
1349 .filter_map(|(idx, outcome)| outcome.result.err().map(|e| (idx, e.to_string())))
1350 .collect(),
1351 Err(e) => indices
1352 .into_iter()
1353 .map(|idx| (idx, e.to_string()))
1354 .collect(),
1355 }
1356}
1357
1358fn receive_command_store_update(cmd: &RefCommand) -> crate::refs::store::RefUpdate {
1359 let mut flags = crate::refs::store::RefUpdateFlags::default();
1360 if cmd.refname == "HEAD" {
1361 flags.no_deref = true;
1362 }
1363 crate::refs::store::RefUpdate {
1364 name: cmd.refname.clone(),
1365 new_value: cmd.new.map(crate::refs::store::RawRef::Direct),
1366 expected: match cmd.old {
1367 Some(oid) => crate::refs::store::Expected::Oid(oid),
1368 None => crate::refs::store::Expected::Missing,
1369 },
1370 reflog: None,
1371 flags,
1372 }
1373}
1374
1375fn relay_hook_sideband(out: &mut Option<&mut dyn Write>, result: &HookResult) -> Result<()> {
1377 let (Some(w), Some(output)) = (out.as_mut(), result.output()) else {
1378 return Ok(());
1379 };
1380 for chunk in output
1381 .stdout
1382 .chunks(65515)
1383 .chain(output.stderr.chunks(65515))
1384 {
1385 crate::pkt_line::write_sideband_packet(*w, 2, chunk).map_err(ServeError::Io)?;
1386 }
1387 Ok(())
1388}
1389
1390fn build_report(
1393 unpack_error: Option<&UnpackError>,
1394 updates: &[RefUpdateResult],
1395) -> Result<Vec<u8>> {
1396 let mut report = Vec::new();
1397 match unpack_error {
1398 None => write_line(&mut report, "unpack ok")?,
1399 Some(e) => write_line(&mut report, &format!("unpack {}", one_line(&e.to_string())))?,
1400 }
1401 for u in updates {
1402 match &u.error {
1403 None => write_line(&mut report, &format!("ok {}", u.refname))?,
1404 Some(reason) => write_line(
1405 &mut report,
1406 &format!("ng {} {}", u.refname, one_line(&reason.to_string())),
1407 )?,
1408 }
1409 }
1410 write_flush(&mut report)?;
1411 Ok(report)
1412}
1413
1414fn one_line(msg: &str) -> String {
1416 msg.lines().next().unwrap_or_default().trim().to_owned()
1417}
1418
1419struct SpooledPack {
1421 file: tempfile::NamedTempFile,
1422 header: [u8; 12],
1424}
1425
1426fn spool_push_pack(
1434 input: &mut dyn Read,
1435 odb: &Odb,
1436 max_bytes: Option<u64>,
1437 stop: &CancelSignal<'_>,
1438) -> std::result::Result<SpooledPack, UnpackError> {
1439 let hash = odb.hash_algo();
1440 let mut file = tempfile::Builder::new()
1441 .prefix("tmp_pack_")
1442 .tempfile_in(odb.objects_dir().join("pack"))?;
1443 let mut scanner = PackStreamScanner::new(hash);
1444 let mut header = [0u8; 12];
1445 let mut chunk = vec![0u8; SPOOL_CHUNK];
1446 let mut written = 0u64;
1447 let mut pack_len: Option<u64> = None;
1448 let mut out = BufWriter::with_capacity(SPOOL_CHUNK, file.as_file_mut());
1449 loop {
1450 stop.poll().map_err(|_| UnpackError::Cancelled)?;
1451 let want = match pack_len {
1452 Some(len) if written >= len => break,
1453 Some(len) => usize::try_from(len - written).map_or(chunk.len(), |n| n.min(chunk.len())),
1454 None => chunk.len(),
1455 };
1456 let n = read_some(input, &mut chunk[..want])?;
1457 if n == 0 {
1458 return Err(UnpackError::Truncated);
1459 }
1460 let mut data = &chunk[..n];
1461 if pack_len.is_none() {
1462 scanner.feed(data)?;
1463 if scanner.is_complete() {
1464 let len = scanner
1465 .pack_body_byte_len()
1466 .saturating_add(hash.len() as u64);
1467 let keep = usize::try_from(len.saturating_sub(written)).map_or(n, |k| k.min(n));
1468 data = &data[..keep];
1469 pack_len = Some(len);
1470 }
1471 }
1472 if let Some(limit) = max_bytes {
1473 let total = pack_len.unwrap_or(written.saturating_add(data.len() as u64));
1474 if total > limit {
1475 return Err(UnpackError::TooLarge { limit });
1476 }
1477 }
1478 if let Some(missing) = usize::try_from(written)
1479 .ok()
1480 .and_then(|start| header.get_mut(start..))
1481 {
1482 let take = missing.len().min(data.len());
1483 missing[..take].copy_from_slice(&data[..take]);
1484 }
1485 out.write_all(data)?;
1486 written += data.len() as u64;
1487 }
1488 out.flush()?;
1489 drop(out);
1490 Ok(SpooledPack { file, header })
1491}
1492
1493fn read_some(input: &mut dyn Read, buf: &mut [u8]) -> std::io::Result<usize> {
1495 loop {
1496 match input.read(buf) {
1497 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1498 other => return other,
1499 }
1500 }
1501}
1502
1503fn check_received_objects(
1508 repo: &Repository,
1509 odb: &Odb,
1510 indexed: Option<&HashSet<ObjectId>>,
1511 cfg: &ConfigSet,
1512 fsck: &ObjectFsckPolicy,
1513) -> std::result::Result<(), UnpackError> {
1514 let checker = fsck.checker(odb.hash_algo(), repo.git_dir(), repo.environment())?;
1515 let mut received: Vec<ObjectId> = match indexed {
1516 Some(ids) => ids.iter().copied().collect(),
1517 None => odb
1518 .enumerate_local_loose_objects()?
1519 .into_iter()
1520 .map(|(oid, _)| oid)
1521 .collect(),
1522 };
1523 received.sort_unstable();
1524 let parallel = repo
1525 .thread_policy()
1526 .try_resolve(ParallelOp::IndexPack, cfg)
1527 .map_err(crate::error::Error::from)?;
1528 let verdicts = try_par_map(&received, parallel, received.len(), |oid| {
1529 if odb.read_info(oid)?.kind == ObjectKind::Blob {
1530 return Ok(None);
1531 }
1532 let obj = odb.read(oid)?;
1533 Ok(checker
1534 .check(oid, obj.kind, &obj.data)
1535 .err()
1536 .map(|error| RejectedObject {
1537 oid: *oid,
1538 kind: obj.kind,
1539 error,
1540 }))
1541 })
1542 .map_err(|ParallelHashError::Task(e): ParallelHashError<crate::error::Error>| e)?;
1543 match verdicts.into_iter().flatten().next() {
1544 Some(rejected) => Err(UnpackError::Rejected(rejected)),
1545 None => Ok(()),
1546 }
1547}
1548
1549fn unconnected_tips(
1555 repo: &Repository,
1556 odb: &Odb,
1557 received: &HashSet<ObjectId>,
1558 existing: &HashSet<ObjectId>,
1559 commands: &[RefCommand],
1560 shallow_boundaries: &HashSet<ObjectId>,
1561 promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1562) -> std::result::Result<HashMap<ObjectId, Unconnected>, UnpackError> {
1563 let mut tips: Vec<ObjectId> = commands.iter().filter_map(|c| c.new).collect();
1564 tips.sort_unstable();
1565 tips.dedup();
1566 if tips.is_empty() {
1567 return Ok(HashMap::new());
1568 }
1569 let receiver_grafts = load_shallow_boundaries(repo.git_dir());
1570 let graph = repo.odb().commit_graph_chain();
1571 let pushed = PushedObjects {
1572 odb,
1573 repo_odb: repo.odb(),
1574 graph: graph.as_deref(),
1575 received,
1576 existing,
1577 shallow_boundaries,
1578 receiver_grafts: &receiver_grafts,
1579 promises,
1580 };
1581 let gaps = pushed.unconnected_tips(&tips)?;
1582 Ok(tips
1583 .into_iter()
1584 .zip(gaps)
1585 .filter_map(|(tip, gap)| Some((tip, gap?)))
1586 .collect())
1587}
1588
1589fn existing_ref_values(repo: &Repository) -> Result<HashSet<ObjectId>> {
1591 let mut existing = super::advertised_object_ids_from_repo(repo, &[], &[], super::Peel::Yes)?;
1592 existing.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
1593 Ok(existing)
1594}
1595
1596fn ingest_spooled_pack(
1608 odb: &Odb,
1609 pack: SpooledPack,
1610 cfg: &ConfigSet,
1611 shallow_boundaries: &HashSet<ObjectId>,
1612 promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1613) -> std::result::Result<Option<HashSet<ObjectId>>, UnpackError> {
1614 if should_use_unpack_objects(&pack.header, cfg) {
1615 let mut reader = BufReader::with_capacity(SPOOL_CHUNK, pack.file.reopen()?);
1616 let opts = UnpackOptions {
1617 quiet: true,
1618 strict: true,
1619 shallow_boundaries: shallow_boundaries.clone(),
1620 ..UnpackOptions::default()
1621 };
1622 match promises {
1623 Some(promises) => {
1624 let promised =
1625 |oid: &ObjectId, referrer: &ObjectId| promises.is_promised(oid, Some(referrer));
1626 crate::unpack_objects::unpack_objects_with_promises(
1627 &mut reader,
1628 odb,
1629 &opts,
1630 &promised,
1631 )?;
1632 }
1633 None => {
1634 unpack_objects(&mut reader, odb, &opts)?;
1635 }
1636 }
1637 return Ok(None);
1638 }
1639 let path = pack.file.into_temp_path();
1640 let installed = install_pack_path(
1641 &path,
1642 odb,
1643 &IngestPackOptions {
1644 fix_thin: true,
1645 skip_post_index_verify: true,
1648 merged_config: Some(cfg.clone()),
1649 ..Default::default()
1650 },
1651 )?;
1652 Ok(Some(installed.object_ids))
1653}
1654
1655#[cfg(test)]
1656mod tests {
1657 use super::*;
1658
1659 #[test]
1660 fn report_lists_unpack_status_then_each_ref() {
1661 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
1662 .unwrap_or_else(|_| ObjectId::zero());
1663 let updates = vec![
1664 RefUpdateResult {
1665 refname: "refs/heads/main".into(),
1666 old: None,
1667 new: Some(oid),
1668 error: None,
1669 },
1670 RefUpdateResult {
1671 refname: "refs/heads/topic".into(),
1672 old: Some(oid),
1673 new: None,
1674 error: Some(RefUpdateRefusal::DeletionProhibited),
1675 },
1676 ];
1677 let report = build_report(None, &updates).unwrap_or_default();
1678 assert_eq!(
1679 String::from_utf8_lossy(&report),
1680 "000eunpack ok\n0017ok refs/heads/main\n002cng refs/heads/topic deletion prohibited\n0000"
1681 );
1682 }
1683
1684 #[test]
1685 fn report_renders_typed_unpack_error_and_refusal() {
1686 let updates = vec![RefUpdateResult {
1687 refname: "refs/heads/main".into(),
1688 old: None,
1689 new: None,
1690 error: Some(RefUpdateRefusal::UnpackerError),
1691 }];
1692 let report = build_report(Some(&UnpackError::Truncated), &updates).unwrap_or_default();
1693 assert_eq!(
1694 String::from_utf8_lossy(&report),
1695 "0029unpack unexpected end of pack stream\n0026ng refs/heads/main unpacker error\n0000"
1696 );
1697 }
1698
1699 #[test]
1700 fn refusals_render_the_reasons_git_clients_expect() {
1701 let cases = [
1702 (RefUpdateRefusal::InvalidRefName, "funny refname"),
1703 (RefUpdateRefusal::HiddenRef, "deny updating a hidden ref"),
1704 (RefUpdateRefusal::StaleInfo, "stale info"),
1705 (RefUpdateRefusal::DeletionProhibited, "deletion prohibited"),
1706 (
1707 RefUpdateRefusal::CurrentBranchDeletion,
1708 "deletion of the current branch prohibited",
1709 ),
1710 (
1711 RefUpdateRefusal::CurrentBranch,
1712 "branch is currently checked out",
1713 ),
1714 (
1715 RefUpdateRefusal::MissingObjects,
1716 "missing necessary objects",
1717 ),
1718 (
1719 RefUpdateRefusal::Unconnected {
1720 missing: ObjectId::zero(),
1721 referrer: ObjectId::zero(),
1722 },
1723 "missing necessary objects",
1724 ),
1725 (RefUpdateRefusal::NonFastForward, "non-fast-forward"),
1726 (RefUpdateRefusal::BadRef, "bad ref"),
1727 (
1728 RefUpdateRefusal::FastForwardCheckFailed,
1729 "unable to verify fast-forward",
1730 ),
1731 (
1732 RefUpdateRefusal::ShallowUpdateNotAllowed,
1733 "shallow update not allowed",
1734 ),
1735 (RefUpdateRefusal::UnpackerError, "unpacker error"),
1736 (
1737 RefUpdateRefusal::AtomicTransactionFailed,
1738 "atomic transaction failed",
1739 ),
1740 (
1741 RefUpdateRefusal::PreReceiveHookDeclined,
1742 "pre-receive hook declined",
1743 ),
1744 (RefUpdateRefusal::UpdateHookDeclined, "hook declined"),
1745 (RefUpdateRefusal::Policy("frozen".into()), "frozen"),
1746 (
1747 RefUpdateRefusal::TargetBranch {
1748 target: "main".into(),
1749 },
1750 "main is the target branch; it changes only through integrate",
1751 ),
1752 (
1753 RefUpdateRefusal::RefStore("locked".into()),
1754 "failed to update ref: locked",
1755 ),
1756 ];
1757 for (refusal, wire) in cases {
1758 assert_eq!(refusal.to_string(), wire);
1759 }
1760 }
1761
1762 struct NoReadPastEnd<'a> {
1765 data: &'a [u8],
1766 }
1767
1768 impl Read for NoReadPastEnd<'_> {
1769 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1770 if self.data.is_empty() {
1771 return Err(std::io::Error::other("read past the end of the pack"));
1772 }
1773 let n = buf.len().min(self.data.len());
1774 buf[..n].copy_from_slice(&self.data[..n]);
1775 self.data = &self.data[n..];
1776 Ok(n)
1777 }
1778 }
1779
1780 fn sample_pack() -> Vec<u8> {
1781 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
1782 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
1783 let mut seed = 0x2545_f491_u32;
1784 let big: Vec<u8> = (0..(SPOOL_CHUNK * 3))
1786 .map(|_| {
1787 seed ^= seed << 13;
1788 seed ^= seed >> 17;
1789 seed ^= seed << 5;
1790 seed as u8
1791 })
1792 .collect();
1793 pb.add_full(PK::Blob, &big);
1794 pb.add_full(PK::Blob, b"small\n");
1795 pb.build().bytes
1796 }
1797
1798 fn test_odb(dir: &std::path::Path) -> Odb {
1799 let objects = dir.join("objects");
1800 std::fs::create_dir_all(objects.join("pack")).unwrap_or_default();
1801 Odb::new(&objects)
1802 }
1803
1804 #[test]
1805 fn spool_stops_at_the_trailer_without_reading_further() {
1806 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1807 let odb = test_odb(tmp.path());
1808 let pack = sample_pack();
1809 let mut input = NoReadPastEnd { data: &pack };
1810 let spooled = spool_push_pack(&mut input, &odb, None, &CancelSignal::never())
1811 .unwrap_or_else(|e| panic!("{e}"));
1812 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1813 assert_eq!(&spooled.header[..], &pack[..12]);
1814 }
1815
1816 #[test]
1817 fn spool_keeps_only_the_pack_when_more_bytes_follow() {
1818 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1819 let odb = test_odb(tmp.path());
1820 let pack = sample_pack();
1821 let mut stream = pack.clone();
1822 stream.extend_from_slice(b"trailing junk");
1823 let spooled = spool_push_pack(&mut stream.as_slice(), &odb, None, &CancelSignal::never())
1824 .unwrap_or_else(|e| panic!("{e}"));
1825 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1826 }
1827
1828 #[test]
1829 fn spool_reports_typed_errors() {
1830 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1831 let odb = test_odb(tmp.path());
1832 let pack = sample_pack();
1833
1834 let truncated = &pack[..pack.len() - 5];
1835 let never = CancelSignal::never();
1836 assert!(matches!(
1837 spool_push_pack(&mut &truncated[..], &odb, None, &never),
1838 Err(UnpackError::Truncated)
1839 ));
1840
1841 let limit = (pack.len() - 1) as u64;
1842 assert!(matches!(
1843 spool_push_pack(&mut pack.as_slice(), &odb, Some(limit), &never),
1844 Err(UnpackError::TooLarge { limit: l }) if l == limit
1845 ));
1846 assert!(
1847 spool_push_pack(&mut pack.as_slice(), &odb, Some(pack.len() as u64), &never).is_ok()
1848 );
1849
1850 assert!(matches!(
1851 spool_push_pack(&mut &b"JUNK\0\0\0\x02\0\0\0\x01"[..], &odb, None, &never),
1852 Err(UnpackError::Malformed(PackStreamScanError::BadSignature))
1853 ));
1854
1855 let leftovers: Vec<_> = std::fs::read_dir(odb.objects_dir().join("pack"))
1856 .map(|rd| rd.filter_map(|e| e.ok()).collect())
1857 .unwrap_or_default();
1858 assert!(
1859 leftovers.is_empty(),
1860 "failed spools must not leave temp files"
1861 );
1862 }
1863
1864 #[test]
1865 fn deny_current_branch_values() {
1866 let policy_for = |value: Option<&str>| {
1867 let mut config = ConfigSet::new();
1868 if let Some(v) = value {
1869 let _ = config.add_command_override("receive.denyCurrentBranch", v);
1870 }
1871 ReceivePolicy::from_config(&config).deny_current_branch
1872 };
1873 assert!(policy_for(None));
1874 assert!(!policy_for(Some("ignore")));
1875 assert!(!policy_for(Some("updateInstead")));
1876 assert!(policy_for(Some("refuse")));
1877 }
1878
1879 #[test]
1880 fn shallow_update_is_off_unless_configured() {
1881 assert!(!ReceivePolicy::default().shallow_update);
1882 let mut config = ConfigSet::new();
1883 let _ = config.add_command_override("receive.shallowUpdate", "true");
1884 assert!(ReceivePolicy::from_config(&config).shallow_update);
1885 }
1886
1887 #[test]
1888 fn deny_delete_current_values() {
1889 let policy_for = |value: Option<&str>| {
1890 let mut config = ConfigSet::new();
1891 if let Some(v) = value {
1892 let _ = config.add_command_override("receive.denyDeleteCurrent", v);
1893 }
1894 ReceivePolicy::from_config(&config).deny_delete_current
1895 };
1896 assert!(policy_for(None));
1897 assert!(!policy_for(Some("ignore")));
1898 assert!(!policy_for(Some("warn")));
1899 assert!(!policy_for(Some("false")));
1900 assert!(policy_for(Some("updateInstead")));
1902 assert!(policy_for(Some("refuse")));
1903 assert!(ReceivePolicy::default().deny_delete_current);
1904 }
1905}