1use std::collections::HashSet;
4use std::io::{BufReader, BufWriter, Read, Write};
5
6use super::{
7 advertised_refs, check_object_format, parse_oid, read_packet, ref_update_policy, write_flush,
8 write_line, Packet, Peel, ProtocolVersion, Result, ServeError, ServeOptions,
9};
10use crate::config::ConfigSet;
11use crate::fsck_msg_policy::{ObjectFsckPolicy, SkipListError};
12use crate::fsck_standalone::FsckError;
13use crate::hooks::{run_hook_in_git_dir, HookResult};
14use crate::index_pack::{install_pack_path, IngestPackOptions};
15use crate::objects::pack_stream_scan::{PackStreamScanError, PackStreamScanner};
16use crate::objects::{ObjectId, ObjectKind};
17use crate::odb::Odb;
18use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
19use crate::receive_pack::{max_input_size_from_config, should_use_unpack_objects};
20use crate::receive_quarantine::ReceiveQuarantine;
21use crate::repo::Repository;
22use crate::shallow::{add_shallow_boundaries, load_shallow_boundaries, PushShallowInfo};
23use crate::unpack_objects::{unpack_objects, UnpackOptions};
24
25const SPOOL_CHUNK: usize = 256 * 1024;
27
28const CAPABILITIES: &[&str] = &[
31 "report-status",
32 "report-status-v2",
33 "delete-refs",
34 "side-band-64k",
35 "quiet",
36 "atomic",
37 "ofs-delta",
38];
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ReceivePolicy {
43 pub deny_non_fast_forwards: bool,
46 pub deny_deletes: bool,
48 pub deny_current_branch: bool,
51 pub deny_delete_current: bool,
54 pub fsck_objects: Option<ObjectFsckPolicy>,
59 pub shallow_update: bool,
67}
68
69impl Default for ReceivePolicy {
70 fn default() -> Self {
71 Self {
72 deny_non_fast_forwards: false,
73 deny_deletes: false,
74 deny_current_branch: true,
75 deny_delete_current: true,
76 fsck_objects: None,
77 shallow_update: false,
78 }
79 }
80}
81
82impl ReceivePolicy {
83 #[must_use]
105 pub fn from_config(config: &ConfigSet) -> Self {
106 let flag = |key: &str| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(false);
107 let allows = |key: &str, update_instead_allows: bool| {
109 config.get(key).is_some_and(|v| {
110 let v = v.to_ascii_lowercase();
111 matches!(v.as_str(), "ignore" | "warn" | "false" | "no" | "off" | "0")
112 || (update_instead_allows && v == "updateinstead")
113 })
114 };
115 let explicit = |key: &str| config.get_bool(key).map(|r| r.unwrap_or(true));
116 let fsck = explicit("receive.fsckObjects")
117 .or_else(|| explicit("transfer.fsckObjects"))
118 .unwrap_or(false);
119 Self {
120 deny_non_fast_forwards: flag("receive.denyNonFastForwards"),
121 deny_deletes: flag("receive.denyDeletes"),
122 deny_current_branch: !allows("receive.denyCurrentBranch", true),
123 deny_delete_current: !allows("receive.denyDeleteCurrent", false),
124 fsck_objects: fsck.then(|| ObjectFsckPolicy::from_config(config, "receive.fsck")),
125 shallow_update: flag("receive.shallowUpdate"),
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct RejectedObject {
133 pub oid: ObjectId,
135 pub kind: ObjectKind,
137 pub error: FsckError,
139}
140
141impl std::fmt::Display for RejectedObject {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 write!(
144 f,
145 "{} {} refused: {}",
146 self.kind,
147 self.oid.to_hex(),
148 self.error.report_line()
149 )
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct RefCommand {
156 pub refname: String,
158 pub old: Option<ObjectId>,
160 pub new: Option<ObjectId>,
162}
163
164impl RefCommand {
165 fn refused(&self, reason: RefUpdateRefusal) -> RefUpdateResult {
166 RefUpdateResult {
167 refname: self.refname.clone(),
168 old: self.old,
169 new: self.new,
170 error: Some(reason),
171 }
172 }
173
174 fn accepted(&self) -> RefUpdateResult {
175 RefUpdateResult {
176 refname: self.refname.clone(),
177 old: self.old,
178 new: self.new,
179 error: None,
180 }
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
190#[non_exhaustive]
191pub enum RefUpdateRefusal {
192 #[error("funny refname")]
194 InvalidRefName,
195 #[error("deny updating a hidden ref")]
197 HiddenRef,
198 #[error("stale info")]
200 StaleInfo,
201 #[error("deletion prohibited")]
203 DeletionProhibited,
204 #[error("deletion of the current branch prohibited")]
207 CurrentBranchDeletion,
208 #[error("branch is currently checked out")]
211 CurrentBranch,
212 #[error("missing necessary objects")]
214 MissingObjects,
215 #[error("non-fast-forward")]
217 NonFastForward,
218 #[error("bad ref")]
221 BadRef,
222 #[error("unable to verify fast-forward")]
226 FastForwardCheckFailed,
227 #[error("shallow update not allowed")]
231 ShallowUpdateNotAllowed,
232 #[error("unpacker error")]
235 UnpackerError,
236 #[error("atomic transaction failed")]
239 AtomicTransactionFailed,
240 #[error("pre-receive hook declined")]
242 PreReceiveHookDeclined,
243 #[error("hook declined")]
245 UpdateHookDeclined,
246 #[error("{0}")]
248 Policy(String),
249 #[error("{target} is the target branch; it changes only through integrate")]
253 TargetBranch {
254 target: String,
256 },
257 #[error("failed to update ref: {0}")]
259 RefStore(String),
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct RefUpdateResult {
265 pub refname: String,
267 pub old: Option<ObjectId>,
269 pub new: Option<ObjectId>,
271 pub error: Option<RefUpdateRefusal>,
273}
274
275#[derive(Debug, thiserror::Error)]
281#[non_exhaustive]
282pub enum UnpackError {
283 #[error("unexpected end of pack stream")]
285 Truncated,
286 #[error("pack exceeds maximum input size of {limit} bytes")]
288 TooLarge {
289 limit: u64,
291 },
292 #[error(transparent)]
294 Malformed(#[from] PackStreamScanError),
295 #[error(transparent)]
298 Ingest(Box<crate::error::Error>),
299 #[error("{0}")]
301 Rejected(RejectedObject),
302 #[error(transparent)]
305 FsckSkipList(#[from] SkipListError),
306 #[error(transparent)]
308 Io(#[from] std::io::Error),
309}
310
311impl From<crate::error::Error> for UnpackError {
312 fn from(err: crate::error::Error) -> Self {
313 Self::Ingest(Box::new(err))
314 }
315}
316
317pub trait ReceiveHook {
328 fn pre_receive(
344 &self,
345 commands: &[RefCommand],
346 odb: &Odb,
347 ) -> std::result::Result<(), RefUpdateRefusal> {
348 let _ = (commands, odb);
349 Ok(())
350 }
351
352 fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
367 let _ = (command, odb);
368 Ok(())
369 }
370
371 fn message(&self, updates: &[RefUpdateResult]) -> Option<String> {
387 let _ = updates;
388 None
389 }
390}
391
392impl ReceiveHook for () {}
393
394impl<F> ReceiveHook for F
395where
396 F: Fn(&RefCommand, &Odb) -> std::result::Result<(), RefUpdateRefusal>,
397{
398 fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
399 self(command, odb)
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct HookRunRecord {
406 pub name: String,
408 pub ran: bool,
410 pub exit_code: Option<i32>,
412}
413
414#[derive(Debug, Default)]
416pub struct ReceiveOutcome {
417 pub unpack_error: Option<UnpackError>,
419 pub updates: Vec<RefUpdateResult>,
421 pub hooks: Vec<HookRunRecord>,
423 pub push_options: Vec<String>,
425}
426
427pub fn receive_pack(
436 repo: &Repository,
437 input: &mut dyn Read,
438 output: &mut dyn Write,
439 opts: &ServeOptions,
440 policy: &ReceivePolicy,
441) -> Result<ReceiveOutcome> {
442 receive_pack_with_hook(repo, input, output, opts, policy, &())
443}
444
445pub fn receive_pack_with_hook(
499 repo: &Repository,
500 input: &mut dyn Read,
501 output: &mut dyn Write,
502 opts: &ServeOptions,
503 policy: &ReceivePolicy,
504 hook: &dyn ReceiveHook,
505) -> Result<ReceiveOutcome> {
506 if opts.advertise_refs || !opts.stateless_rpc {
507 if opts.protocol == ProtocolVersion::V1 {
508 write_line(output, "version 1")?;
509 }
510 write_advertisement(repo, output, opts)?;
511 output.flush()?;
512 if opts.advertise_refs {
513 return Ok(ReceiveOutcome::default());
514 }
515 }
516
517 let Some(request) = read_commands(repo, input)? else {
518 return Ok(ReceiveOutcome::default());
519 };
520
521 let cfg = repo
522 .config()
523 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
524 let max_input = max_input_size_from_config(cfg.as_ref());
525 let sideband = request.caps.contains("side-band-64k");
526
527 let needs_pack = request.commands.iter().any(|c| c.new.is_some());
528 let mut quarantine = if needs_pack {
529 Some(ReceiveQuarantine::create(repo.odb(), repo.git_dir())?)
530 } else {
531 None
532 };
533
534 let receiver_grafts = if request.shallow.is_empty() {
535 HashSet::new()
536 } else {
537 load_shallow_boundaries(repo.git_dir())
538 };
539 let mut shallow_info =
540 PushShallowInfo::classify(&request.shallow, repo.odb(), &receiver_grafts)?;
541 let client_boundaries: HashSet<ObjectId> = request.shallow.iter().copied().collect();
542
543 let unpack_error = match quarantine.as_ref() {
544 Some(q) => {
545 let odb = q.odb();
546 spool_push_pack(input, &odb, max_input)
547 .and_then(|pack| ingest_spooled_pack(&odb, pack, cfg.as_ref(), &client_boundaries))
548 .and_then(|indexed| match &policy.fsck_objects {
549 Some(fsck) => check_received_objects(repo, &odb, indexed, cfg.as_ref(), fsck),
550 None => Ok(()),
551 })
552 .err()
553 }
554 None => None,
555 };
556
557 let shallow = match quarantine.as_ref() {
558 Some(q) if unpack_error.is_none() => {
559 shallow_info.retain_received(&q.odb())?;
560 ShallowPlan::new(
561 repo,
562 &q.odb(),
563 &request,
564 policy,
565 &shallow_info,
566 &receiver_grafts,
567 )?
568 }
569 _ => ShallowPlan::none(request.commands.len()),
570 };
571
572 let mut hook_records = Vec::new();
573 let updates = if unpack_error.is_some() {
574 request
575 .commands
576 .iter()
577 .map(|c| c.refused(RefUpdateRefusal::UnpackerError))
578 .collect()
579 } else {
580 apply_commands_with_hooks(
581 repo,
582 &request,
583 opts,
584 policy,
585 hook,
586 quarantine.as_mut(),
587 &shallow,
588 sideband.then_some(&mut *output),
589 &mut hook_records,
590 )?
591 };
592
593 if sideband {
594 if let Some(text) = hook.message(&updates) {
595 write_progress_text(output, &text)?;
596 }
597 }
598 if request.caps.contains("report-status") || request.caps.contains("report-status-v2") {
599 let report = build_report(unpack_error.as_ref(), &updates)?;
600 if sideband {
601 crate::pkt_line::write_sideband_channel1_64k(&mut &mut *output, &report)?;
602 write_flush(output)?;
603 } else {
604 output.write_all(&report)?;
605 }
606 }
607 output.flush()?;
608
609 Ok(ReceiveOutcome {
610 unpack_error,
611 updates,
612 hooks: hook_records,
613 push_options: request.push_options.clone(),
614 })
615}
616
617fn write_progress_text(output: &mut dyn Write, text: &str) -> Result<()> {
620 for line in text.lines() {
621 let line = format!("{line}\n");
622 for chunk in line.as_bytes().chunks(65515) {
623 crate::pkt_line::write_sideband_packet(&mut *output, 2, chunk)
624 .map_err(ServeError::Io)?;
625 }
626 }
627 Ok(())
628}
629
630fn write_advertisement(
632 repo: &Repository,
633 output: &mut dyn Write,
634 opts: &ServeOptions,
635) -> Result<()> {
636 let mut caps: Vec<String> = CAPABILITIES.iter().map(|c| (*c).to_owned()).collect();
637 let config = repo
638 .config()
639 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
640 if config
641 .get_bool("receive.advertisepushoptions")
642 .and_then(|r| r.ok())
643 .unwrap_or(false)
644 {
645 caps.push("push-options".to_owned());
646 }
647 caps.push(format!("object-format={}", repo.odb().hash_algo().name()));
648 if let Some(path) = &opts.push_bundle {
649 caps.push(format!("{}={path}", super::PUSH_BUNDLE_CAPABILITY));
650 }
651 if !opts.agent.is_empty() {
652 caps.push(format!("agent={}", opts.agent));
653 }
654 let caps = caps.join(" ");
655
656 let refs = advertised_refs(repo, &opts.hidden_refs, &[], Peel::No)?;
657 if refs.is_empty() {
658 let zero = ObjectId::null(repo.odb().hash_algo());
659 write_line(
660 output,
661 &format!("{} capabilities^{{}}\0{caps}", zero.to_hex()),
662 )?;
663 }
664 for (i, r) in refs.iter().enumerate() {
665 if i == 0 {
666 write_line(output, &format!("{} {}\0{caps}", r.oid.to_hex(), r.name))?;
667 } else {
668 write_line(output, &format!("{} {}", r.oid.to_hex(), r.name))?;
669 }
670 }
671 write_flush(output)
672}
673
674struct PushRequest {
676 commands: Vec<RefCommand>,
677 caps: HashSet<String>,
678 push_options: Vec<String>,
679 shallow: Vec<ObjectId>,
681}
682
683fn read_commands(repo: &Repository, input: &mut dyn Read) -> Result<Option<PushRequest>> {
689 let mut commands = Vec::new();
690 let mut caps = HashSet::new();
691 let mut shallow = Vec::new();
692 loop {
693 match read_packet(input)? {
694 None | Some(Packet::Flush) => break,
695 Some(Packet::Data(line)) => {
696 if let Some(hex) = line.strip_prefix("shallow ") {
697 shallow.push(parse_oid(repo, hex)?);
698 continue;
699 }
700 let (line, line_caps) = line.split_once('\0').unwrap_or((line.as_str(), ""));
701 if commands.is_empty() {
702 caps.extend(
703 line_caps
704 .split(' ')
705 .filter(|c| !c.is_empty())
706 .map(str::to_owned),
707 );
708 }
709 let mut parts = line.splitn(3, ' ');
710 let (Some(old), Some(new), Some(refname)) =
711 (parts.next(), parts.next(), parts.next())
712 else {
713 return Err(ServeError::Protocol(format!("malformed command '{line}'")));
714 };
715 let old = parse_oid(repo, old)?;
716 let new = parse_oid(repo, new)?;
717 commands.push(RefCommand {
718 old: (!old.is_zero()).then_some(old),
719 new: (!new.is_zero()).then_some(new),
720 refname: refname.to_owned(),
721 });
722 }
723 Some(other) => {
724 return Err(ServeError::Protocol(format!(
725 "unexpected packet in command list: {other:?}"
726 )))
727 }
728 }
729 }
730 if commands.is_empty() {
731 return Ok(None);
732 }
733 let mut push_options = Vec::new();
734 if caps.contains("push-options") {
735 loop {
736 match read_packet(input)? {
737 None | Some(Packet::Flush) => break,
738 Some(Packet::Data(line)) => {
739 push_options.push(line);
740 }
741 Some(other) => {
742 return Err(ServeError::Protocol(format!(
743 "unexpected packet in push options: {other:?}"
744 )))
745 }
746 }
747 }
748 }
749 let format = caps.iter().find_map(|c| c.strip_prefix("object-format="));
750 check_object_format(repo, format)?;
751 Ok(Some(PushRequest {
752 commands,
753 caps,
754 push_options,
755 shallow,
756 }))
757}
758
759struct ShallowPlan {
761 roots: Vec<Vec<ObjectId>>,
764 hook_file: Option<tempfile::NamedTempFile>,
768}
769
770impl ShallowPlan {
771 fn none(commands: usize) -> Self {
773 Self {
774 roots: vec![Vec::new(); commands],
775 hook_file: None,
776 }
777 }
778
779 fn new(
782 repo: &Repository,
783 odb: &Odb,
784 request: &PushRequest,
785 policy: &ReceivePolicy,
786 info: &PushShallowInfo,
787 receiver_grafts: &HashSet<ObjectId>,
788 ) -> Result<Self> {
789 if info.is_empty() {
790 return Ok(Self::none(request.commands.len()));
791 }
792 let mut receiver_tips: HashSet<ObjectId> = advertised_refs(repo, &[], &[], Peel::Yes)?
793 .into_iter()
794 .map(|r| r.peeled.unwrap_or(r.oid))
795 .collect();
796 receiver_tips.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
797 let graph = repo.caches().commit_graph_chain(repo.odb().objects_dir());
798 let roots = request
799 .commands
800 .iter()
801 .map(|c| match c.new {
802 Some(new) => {
803 info.new_roots(odb, graph.as_deref(), new, receiver_grafts, &receiver_tips)
804 }
805 None => Ok(Vec::new()),
806 })
807 .collect::<crate::error::Result<_>>()?;
808 let hook_file = if policy.shallow_update {
809 Some(temporary_shallow_file(
810 repo,
811 receiver_grafts,
812 &request.shallow,
813 )?)
814 } else {
815 None
816 };
817 Ok(Self { roots, hook_file })
818 }
819
820 fn hook_env(&self) -> Result<Vec<(String, String)>> {
822 let Some(file) = &self.hook_file else {
823 return Ok(Vec::new());
824 };
825 let path = std::path::absolute(file.path())?;
826 Ok(vec![(
827 "GIT_SHALLOW_FILE".to_owned(),
828 path.to_string_lossy().into_owned(),
829 )])
830 }
831}
832
833fn temporary_shallow_file(
836 repo: &Repository,
837 receiver_grafts: &HashSet<ObjectId>,
838 client: &[ObjectId],
839) -> Result<tempfile::NamedTempFile> {
840 let mut hexes: Vec<String> = receiver_grafts
841 .iter()
842 .chain(client)
843 .map(ObjectId::to_hex)
844 .collect();
845 hexes.sort_unstable();
846 hexes.dedup();
847 let dir = crate::shallow::shallow_file_path(repo.git_dir())
848 .parent()
849 .map(std::path::Path::to_path_buf)
850 .unwrap_or_else(|| repo.git_dir().to_path_buf());
851 let mut file = tempfile::Builder::new()
852 .prefix("shallow_")
853 .tempfile_in(dir)?;
854 for hex in hexes {
855 writeln!(file, "{hex}")?;
856 }
857 file.flush()?;
858 Ok(file)
859}
860
861#[allow(clippy::too_many_arguments)]
863fn apply_commands_with_hooks(
864 repo: &Repository,
865 request: &PushRequest,
866 opts: &ServeOptions,
867 policy: &ReceivePolicy,
868 hook: &dyn ReceiveHook,
869 quarantine: Option<&mut ReceiveQuarantine>,
870 shallow: &ShallowPlan,
871 mut sideband_out: Option<&mut dyn Write>,
872 hook_records: &mut Vec<HookRunRecord>,
873) -> Result<Vec<RefUpdateResult>> {
874 let lookup_odb = quarantine.as_ref().map(|q| q.odb());
875 let head_branch = ref_update_policy::head_branch(repo);
876 let checked_out = ref_update_policy::checked_out_branch(repo);
877 let branches = ref_update_policy::HeadBranches {
878 head: head_branch.as_deref(),
879 checked_out: checked_out.as_deref(),
880 };
881 let mut results: Vec<RefUpdateResult> = request
882 .commands
883 .iter()
884 .zip(&shallow.roots)
885 .map(|(c, roots)| {
886 if !roots.is_empty() && !policy.shallow_update {
887 return Ok(c.refused(RefUpdateRefusal::ShallowUpdateNotAllowed));
888 }
889 Ok(
890 match ref_update_policy::validate_ref_update(
891 repo,
892 lookup_odb.as_ref(),
893 c,
894 policy,
895 &opts.hidden_refs,
896 branches,
897 )? {
898 Some(reason) => c.refused(reason),
899 None => c.accepted(),
900 },
901 )
902 })
903 .collect::<Result<_>>()?;
904
905 let atomic = request.caps.contains("atomic");
906 if atomic && results.iter().any(|r| r.error.is_some()) {
907 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
908 return Ok(results);
909 }
910
911 let accepted: Vec<RefCommand> = request
912 .commands
913 .iter()
914 .zip(&results)
915 .filter(|(_, r)| r.error.is_none())
916 .map(|(c, _)| c.clone())
917 .collect();
918 if !accepted.is_empty() {
919 if let Err(refusal) = hook.pre_receive(&accepted, lookup_odb.as_ref().unwrap_or(repo.odb()))
920 {
921 refuse_remaining(&mut results, &refusal);
922 return Ok(results);
923 }
924 }
925
926 let algo = repo.odb().hash_algo();
927 let shallow_env = shallow.hook_env()?;
928 let mut push_option_env = push_option_env_owned(&request.push_options);
929 push_option_env.extend(shallow_env.iter().cloned());
930 let mut pre_hook_env_owned = push_option_env.clone();
931 if let Some(q) = quarantine.as_ref() {
932 pre_hook_env_owned.extend(q.hook_env());
933 }
934 let pre_hook_env: Vec<(&str, &str)> = pre_hook_env_owned
935 .iter()
936 .map(|(k, v)| (k.as_str(), v.as_str()))
937 .collect();
938 let update_hook_env: Vec<(&str, &str)> = shallow_env
939 .iter()
940 .map(|(k, v)| (k.as_str(), v.as_str()))
941 .collect();
942 let update_hook_env = update_hook_env.as_slice();
943 let post_receive_env_owned = push_option_env;
944 let post_receive_env: Vec<(&str, &str)> = post_receive_env_owned
945 .iter()
946 .map(|(k, v)| (k.as_str(), v.as_str()))
947 .collect();
948
949 let all_cmds: Vec<&RefCommand> = request.commands.iter().collect();
950 let pre_stdin = commands_stdin(&all_cmds, algo);
951 let pre_result = run_hook_in_git_dir(
952 repo,
953 "pre-receive",
954 &[],
955 Some(pre_stdin.as_bytes()),
956 &pre_hook_env,
957 );
958 relay_hook_sideband(&mut sideband_out, &pre_result)?;
959 hook_records.push(hook_record("pre-receive", &pre_result));
960 if !pre_result.is_ok() {
961 refuse_remaining(&mut results, &RefUpdateRefusal::PreReceiveHookDeclined);
962 return Ok(results);
963 }
964
965 if results.iter().all(|r| r.error.is_some()) {
966 return Ok(results);
967 }
968
969 if let Some(q) = quarantine {
970 q.migrate().map_err(ServeError::Repository)?;
971 repo.odb().invalidate_packs();
972 }
973
974 for (cmd, result) in request.commands.iter().zip(results.iter_mut()) {
975 if result.error.is_some() {
976 continue;
977 }
978 if let Err(refusal) = hook.update(cmd, repo.odb()) {
979 result.error = Some(refusal);
980 continue;
981 }
982 let zero = null_oid_hex(algo);
983 let old_hex = cmd.old.map(|o| o.to_hex()).unwrap_or_else(|| zero.clone());
984 let new_hex = cmd.new.map(|o| o.to_hex()).unwrap_or(zero.clone());
985 let args = [cmd.refname.as_str(), old_hex.as_str(), new_hex.as_str()];
986 let update_result = run_hook_in_git_dir(repo, "update", &args, None, update_hook_env);
987 relay_hook_sideband(&mut sideband_out, &update_result)?;
988 hook_records.push(hook_record("update", &update_result));
989 if !update_result.is_ok() {
990 result.error = Some(RefUpdateRefusal::UpdateHookDeclined);
991 }
992 }
993
994 if atomic && results.iter().any(|r| r.error.is_some()) {
995 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
996 return Ok(results);
997 }
998
999 let new_roots: HashSet<ObjectId> = shallow
1000 .roots
1001 .iter()
1002 .zip(&results)
1003 .filter(|(_, r)| r.error.is_none())
1004 .flat_map(|(roots, _)| roots.iter().copied())
1005 .collect();
1006 if let Err(e) = add_shallow_boundaries(repo.git_dir(), &new_roots) {
1007 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1008 for (roots, result) in shallow.roots.iter().zip(results.iter_mut()) {
1009 if !roots.is_empty() && result.error.is_none() {
1010 result.error = Some(refusal.clone());
1011 }
1012 }
1013 if atomic {
1014 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1015 return Ok(results);
1016 }
1017 }
1018
1019 let pending: Vec<(usize, crate::refs::store::RefUpdate)> = request
1020 .commands
1021 .iter()
1022 .zip(results.iter())
1023 .enumerate()
1024 .filter(|(_, (_, r))| r.error.is_none())
1025 .map(|(idx, (cmd, _))| (idx, receive_command_store_update(cmd)))
1026 .collect();
1027
1028 if atomic {
1029 let batch: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1030 if let Err(e) = crate::refs::commit_ref_store_batch(repo.git_dir(), &batch) {
1031 let refusal = RefUpdateRefusal::RefStore(e.to_string());
1032 for (idx, _) in pending {
1033 if results[idx].error.is_none() {
1034 results[idx].error = Some(refusal.clone());
1035 }
1036 }
1037 }
1038 } else {
1039 for (idx, update) in pending {
1040 if let Err(e) = crate::refs::commit_store_update(repo.git_dir(), update) {
1041 results[idx].error = Some(RefUpdateRefusal::RefStore(e.to_string()));
1042 }
1043 }
1044 }
1045
1046 let successful: Vec<&RefCommand> = request
1047 .commands
1048 .iter()
1049 .zip(results.iter())
1050 .filter(|(_, r)| r.error.is_none())
1051 .map(|(c, _)| c)
1052 .collect();
1053 if !successful.is_empty() {
1054 let post_stdin = commands_stdin(&successful, algo);
1055 let post_result = run_hook_in_git_dir(
1056 repo,
1057 "post-receive",
1058 &[],
1059 Some(post_stdin.as_bytes()),
1060 &post_receive_env,
1061 );
1062 relay_hook_sideband(&mut sideband_out, &post_result)?;
1063 hook_records.push(hook_record("post-receive", &post_result));
1064
1065 let mut post_update_args: Vec<&str> = Vec::new();
1066 for result in results.iter().filter(|r| r.error.is_none()) {
1067 post_update_args.push(result.refname.as_str());
1068 }
1069 let pu_result = run_hook_in_git_dir(
1070 repo,
1071 "post-update",
1072 &post_update_args,
1073 None,
1074 update_hook_env,
1075 );
1076 relay_hook_sideband(&mut sideband_out, &pu_result)?;
1077 hook_records.push(hook_record("post-update", &pu_result));
1078 }
1079
1080 Ok(results)
1081}
1082
1083fn refuse_remaining(results: &mut [RefUpdateResult], reason: &RefUpdateRefusal) {
1085 for r in results.iter_mut().filter(|r| r.error.is_none()) {
1086 r.error = Some(reason.clone());
1087 }
1088}
1089
1090fn hook_record(name: &str, result: &HookResult) -> HookRunRecord {
1091 HookRunRecord {
1092 name: name.to_owned(),
1093 ran: result.was_executed(),
1094 exit_code: result.status(),
1095 }
1096}
1097
1098fn push_option_env_owned(options: &[String]) -> Vec<(String, String)> {
1099 let mut pairs = vec![(
1100 "GIT_PUSH_OPTION_COUNT".to_owned(),
1101 options.len().to_string(),
1102 )];
1103 for (i, opt) in options.iter().enumerate() {
1104 pairs.push((format!("GIT_PUSH_OPTION_{i}"), opt.clone()));
1105 }
1106 pairs
1107}
1108
1109fn commands_stdin(commands: &[&RefCommand], algo: crate::objects::HashAlgo) -> String {
1110 let zero = null_oid_hex(algo);
1111 commands
1112 .iter()
1113 .map(|c| {
1114 format!(
1115 "{} {} {}\n",
1116 c.old.map(|o| o.to_hex()).unwrap_or_else(|| zero.clone()),
1117 c.new.map(|o| o.to_hex()).unwrap_or_else(|| zero.clone()),
1118 c.refname
1119 )
1120 })
1121 .collect()
1122}
1123
1124fn null_oid_hex(algo: crate::objects::HashAlgo) -> String {
1125 ObjectId::null(algo).to_hex()
1126}
1127
1128fn receive_command_store_update(cmd: &RefCommand) -> crate::refs::store::RefUpdate {
1129 let mut flags = crate::refs::store::RefUpdateFlags::default();
1130 if cmd.refname == "HEAD" {
1131 flags.no_deref = true;
1132 }
1133 crate::refs::store::RefUpdate {
1134 name: cmd.refname.clone(),
1135 new_value: cmd.new.map(crate::refs::store::RawRef::Direct),
1136 expected: match cmd.old {
1137 Some(oid) => crate::refs::store::Expected::Oid(oid),
1138 None => crate::refs::store::Expected::Missing,
1139 },
1140 reflog: None,
1141 flags,
1142 }
1143}
1144
1145fn relay_hook_sideband(out: &mut Option<&mut dyn Write>, result: &HookResult) -> Result<()> {
1147 let (Some(w), Some(output)) = (out.as_mut(), result.output()) else {
1148 return Ok(());
1149 };
1150 for chunk in output
1151 .stdout
1152 .chunks(65515)
1153 .chain(output.stderr.chunks(65515))
1154 {
1155 crate::pkt_line::write_sideband_packet(*w, 2, chunk).map_err(ServeError::Io)?;
1156 }
1157 Ok(())
1158}
1159
1160fn build_report(
1163 unpack_error: Option<&UnpackError>,
1164 updates: &[RefUpdateResult],
1165) -> Result<Vec<u8>> {
1166 let mut report = Vec::new();
1167 match unpack_error {
1168 None => write_line(&mut report, "unpack ok")?,
1169 Some(e) => write_line(&mut report, &format!("unpack {}", one_line(&e.to_string())))?,
1170 }
1171 for u in updates {
1172 match &u.error {
1173 None => write_line(&mut report, &format!("ok {}", u.refname))?,
1174 Some(reason) => write_line(
1175 &mut report,
1176 &format!("ng {} {}", u.refname, one_line(&reason.to_string())),
1177 )?,
1178 }
1179 }
1180 write_flush(&mut report)?;
1181 Ok(report)
1182}
1183
1184fn one_line(msg: &str) -> String {
1186 msg.lines().next().unwrap_or_default().trim().to_owned()
1187}
1188
1189struct SpooledPack {
1191 file: tempfile::NamedTempFile,
1192 header: [u8; 12],
1194}
1195
1196fn spool_push_pack(
1204 input: &mut dyn Read,
1205 odb: &Odb,
1206 max_bytes: Option<u64>,
1207) -> std::result::Result<SpooledPack, UnpackError> {
1208 let hash = odb.hash_algo();
1209 let mut file = tempfile::Builder::new()
1210 .prefix("tmp_pack_")
1211 .tempfile_in(odb.objects_dir().join("pack"))?;
1212 let mut scanner = PackStreamScanner::new(hash);
1213 let mut header = [0u8; 12];
1214 let mut chunk = vec![0u8; SPOOL_CHUNK];
1215 let mut written = 0u64;
1216 let mut pack_len: Option<u64> = None;
1217 let mut out = BufWriter::with_capacity(SPOOL_CHUNK, file.as_file_mut());
1218 loop {
1219 let want = match pack_len {
1220 Some(len) if written >= len => break,
1221 Some(len) => usize::try_from(len - written).map_or(chunk.len(), |n| n.min(chunk.len())),
1222 None => chunk.len(),
1223 };
1224 let n = read_some(input, &mut chunk[..want])?;
1225 if n == 0 {
1226 return Err(UnpackError::Truncated);
1227 }
1228 let mut data = &chunk[..n];
1229 if pack_len.is_none() {
1230 scanner.feed(data)?;
1231 if scanner.is_complete() {
1232 let len = scanner
1233 .pack_body_byte_len()
1234 .saturating_add(hash.len() as u64);
1235 let keep = usize::try_from(len.saturating_sub(written)).map_or(n, |k| k.min(n));
1236 data = &data[..keep];
1237 pack_len = Some(len);
1238 }
1239 }
1240 if let Some(limit) = max_bytes {
1241 let total = pack_len.unwrap_or(written.saturating_add(data.len() as u64));
1242 if total > limit {
1243 return Err(UnpackError::TooLarge { limit });
1244 }
1245 }
1246 if let Some(missing) = usize::try_from(written)
1247 .ok()
1248 .and_then(|start| header.get_mut(start..))
1249 {
1250 let take = missing.len().min(data.len());
1251 missing[..take].copy_from_slice(&data[..take]);
1252 }
1253 out.write_all(data)?;
1254 written += data.len() as u64;
1255 }
1256 out.flush()?;
1257 drop(out);
1258 Ok(SpooledPack { file, header })
1259}
1260
1261fn read_some(input: &mut dyn Read, buf: &mut [u8]) -> std::io::Result<usize> {
1263 loop {
1264 match input.read(buf) {
1265 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1266 other => return other,
1267 }
1268 }
1269}
1270
1271fn check_received_objects(
1276 repo: &Repository,
1277 odb: &Odb,
1278 indexed: Option<HashSet<ObjectId>>,
1279 cfg: &ConfigSet,
1280 fsck: &ObjectFsckPolicy,
1281) -> std::result::Result<(), UnpackError> {
1282 let checker = fsck.checker(odb.hash_algo(), repo.git_dir(), repo.environment())?;
1283 let mut received: Vec<ObjectId> = match indexed {
1284 Some(ids) => ids.into_iter().collect(),
1285 None => odb
1286 .enumerate_local_loose_objects()?
1287 .into_iter()
1288 .map(|(oid, _)| oid)
1289 .collect(),
1290 };
1291 received.sort_unstable();
1292 let parallel = repo
1293 .thread_policy()
1294 .try_resolve(ParallelOp::IndexPack, cfg)
1295 .map_err(crate::error::Error::from)?;
1296 let verdicts = try_par_map(&received, parallel, received.len(), |oid| {
1297 if odb.read_info(oid)?.kind == ObjectKind::Blob {
1298 return Ok(None);
1299 }
1300 let obj = odb.read(oid)?;
1301 Ok(checker
1302 .check(oid, obj.kind, &obj.data)
1303 .err()
1304 .map(|error| RejectedObject {
1305 oid: *oid,
1306 kind: obj.kind,
1307 error,
1308 }))
1309 })
1310 .map_err(|ParallelHashError::Task(e): ParallelHashError<crate::error::Error>| e)?;
1311 match verdicts.into_iter().flatten().next() {
1312 Some(rejected) => Err(UnpackError::Rejected(rejected)),
1313 None => Ok(()),
1314 }
1315}
1316
1317fn ingest_spooled_pack(
1327 odb: &Odb,
1328 pack: SpooledPack,
1329 cfg: &ConfigSet,
1330 shallow_boundaries: &HashSet<ObjectId>,
1331) -> std::result::Result<Option<HashSet<ObjectId>>, UnpackError> {
1332 if should_use_unpack_objects(&pack.header, cfg) {
1333 let mut reader = BufReader::with_capacity(SPOOL_CHUNK, pack.file.reopen()?);
1334 unpack_objects(
1335 &mut reader,
1336 odb,
1337 &UnpackOptions {
1338 quiet: true,
1339 strict: true,
1340 shallow_boundaries: shallow_boundaries.clone(),
1341 ..UnpackOptions::default()
1342 },
1343 )?;
1344 return Ok(None);
1345 }
1346 let path = pack.file.into_temp_path();
1347 let installed = install_pack_path(
1348 &path,
1349 odb,
1350 &IngestPackOptions {
1351 fix_thin: true,
1352 skip_post_index_verify: true,
1355 ..Default::default()
1356 },
1357 )?;
1358 Ok(Some(installed.object_ids))
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363 use super::*;
1364
1365 #[test]
1366 fn report_lists_unpack_status_then_each_ref() {
1367 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
1368 .unwrap_or_else(|_| ObjectId::zero());
1369 let updates = vec![
1370 RefUpdateResult {
1371 refname: "refs/heads/main".into(),
1372 old: None,
1373 new: Some(oid),
1374 error: None,
1375 },
1376 RefUpdateResult {
1377 refname: "refs/heads/topic".into(),
1378 old: Some(oid),
1379 new: None,
1380 error: Some(RefUpdateRefusal::DeletionProhibited),
1381 },
1382 ];
1383 let report = build_report(None, &updates).unwrap_or_default();
1384 assert_eq!(
1385 String::from_utf8_lossy(&report),
1386 "000eunpack ok\n0017ok refs/heads/main\n002cng refs/heads/topic deletion prohibited\n0000"
1387 );
1388 }
1389
1390 #[test]
1391 fn report_renders_typed_unpack_error_and_refusal() {
1392 let updates = vec![RefUpdateResult {
1393 refname: "refs/heads/main".into(),
1394 old: None,
1395 new: None,
1396 error: Some(RefUpdateRefusal::UnpackerError),
1397 }];
1398 let report = build_report(Some(&UnpackError::Truncated), &updates).unwrap_or_default();
1399 assert_eq!(
1400 String::from_utf8_lossy(&report),
1401 "0029unpack unexpected end of pack stream\n0026ng refs/heads/main unpacker error\n0000"
1402 );
1403 }
1404
1405 #[test]
1406 fn refusals_render_the_reasons_git_clients_expect() {
1407 let cases = [
1408 (RefUpdateRefusal::InvalidRefName, "funny refname"),
1409 (RefUpdateRefusal::HiddenRef, "deny updating a hidden ref"),
1410 (RefUpdateRefusal::StaleInfo, "stale info"),
1411 (RefUpdateRefusal::DeletionProhibited, "deletion prohibited"),
1412 (
1413 RefUpdateRefusal::CurrentBranchDeletion,
1414 "deletion of the current branch prohibited",
1415 ),
1416 (
1417 RefUpdateRefusal::CurrentBranch,
1418 "branch is currently checked out",
1419 ),
1420 (
1421 RefUpdateRefusal::MissingObjects,
1422 "missing necessary objects",
1423 ),
1424 (RefUpdateRefusal::NonFastForward, "non-fast-forward"),
1425 (RefUpdateRefusal::BadRef, "bad ref"),
1426 (
1427 RefUpdateRefusal::FastForwardCheckFailed,
1428 "unable to verify fast-forward",
1429 ),
1430 (
1431 RefUpdateRefusal::ShallowUpdateNotAllowed,
1432 "shallow update not allowed",
1433 ),
1434 (RefUpdateRefusal::UnpackerError, "unpacker error"),
1435 (
1436 RefUpdateRefusal::AtomicTransactionFailed,
1437 "atomic transaction failed",
1438 ),
1439 (
1440 RefUpdateRefusal::PreReceiveHookDeclined,
1441 "pre-receive hook declined",
1442 ),
1443 (RefUpdateRefusal::UpdateHookDeclined, "hook declined"),
1444 (RefUpdateRefusal::Policy("frozen".into()), "frozen"),
1445 (
1446 RefUpdateRefusal::TargetBranch {
1447 target: "main".into(),
1448 },
1449 "main is the target branch; it changes only through integrate",
1450 ),
1451 (
1452 RefUpdateRefusal::RefStore("locked".into()),
1453 "failed to update ref: locked",
1454 ),
1455 ];
1456 for (refusal, wire) in cases {
1457 assert_eq!(refusal.to_string(), wire);
1458 }
1459 }
1460
1461 struct NoReadPastEnd<'a> {
1464 data: &'a [u8],
1465 }
1466
1467 impl Read for NoReadPastEnd<'_> {
1468 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1469 if self.data.is_empty() {
1470 return Err(std::io::Error::other("read past the end of the pack"));
1471 }
1472 let n = buf.len().min(self.data.len());
1473 buf[..n].copy_from_slice(&self.data[..n]);
1474 self.data = &self.data[n..];
1475 Ok(n)
1476 }
1477 }
1478
1479 fn sample_pack() -> Vec<u8> {
1480 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
1481 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
1482 let mut seed = 0x2545_f491_u32;
1483 let big: Vec<u8> = (0..(SPOOL_CHUNK * 3))
1485 .map(|_| {
1486 seed ^= seed << 13;
1487 seed ^= seed >> 17;
1488 seed ^= seed << 5;
1489 seed as u8
1490 })
1491 .collect();
1492 pb.add_full(PK::Blob, &big);
1493 pb.add_full(PK::Blob, b"small\n");
1494 pb.build().bytes
1495 }
1496
1497 fn test_odb(dir: &std::path::Path) -> Odb {
1498 let objects = dir.join("objects");
1499 std::fs::create_dir_all(objects.join("pack")).unwrap_or_default();
1500 Odb::new(&objects)
1501 }
1502
1503 #[test]
1504 fn spool_stops_at_the_trailer_without_reading_further() {
1505 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1506 let odb = test_odb(tmp.path());
1507 let pack = sample_pack();
1508 let mut input = NoReadPastEnd { data: &pack };
1509 let spooled = spool_push_pack(&mut input, &odb, None).unwrap_or_else(|e| panic!("{e}"));
1510 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1511 assert_eq!(&spooled.header[..], &pack[..12]);
1512 }
1513
1514 #[test]
1515 fn spool_keeps_only_the_pack_when_more_bytes_follow() {
1516 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1517 let odb = test_odb(tmp.path());
1518 let pack = sample_pack();
1519 let mut stream = pack.clone();
1520 stream.extend_from_slice(b"trailing junk");
1521 let spooled =
1522 spool_push_pack(&mut stream.as_slice(), &odb, None).unwrap_or_else(|e| panic!("{e}"));
1523 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1524 }
1525
1526 #[test]
1527 fn spool_reports_typed_errors() {
1528 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1529 let odb = test_odb(tmp.path());
1530 let pack = sample_pack();
1531
1532 let truncated = &pack[..pack.len() - 5];
1533 assert!(matches!(
1534 spool_push_pack(&mut &truncated[..], &odb, None),
1535 Err(UnpackError::Truncated)
1536 ));
1537
1538 let limit = (pack.len() - 1) as u64;
1539 assert!(matches!(
1540 spool_push_pack(&mut pack.as_slice(), &odb, Some(limit)),
1541 Err(UnpackError::TooLarge { limit: l }) if l == limit
1542 ));
1543 assert!(spool_push_pack(&mut pack.as_slice(), &odb, Some(pack.len() as u64)).is_ok());
1544
1545 assert!(matches!(
1546 spool_push_pack(&mut &b"JUNK\0\0\0\x02\0\0\0\x01"[..], &odb, None),
1547 Err(UnpackError::Malformed(PackStreamScanError::BadSignature))
1548 ));
1549
1550 let leftovers: Vec<_> = std::fs::read_dir(odb.objects_dir().join("pack"))
1551 .map(|rd| rd.filter_map(|e| e.ok()).collect())
1552 .unwrap_or_default();
1553 assert!(
1554 leftovers.is_empty(),
1555 "failed spools must not leave temp files"
1556 );
1557 }
1558
1559 #[test]
1560 fn deny_current_branch_values() {
1561 let policy_for = |value: Option<&str>| {
1562 let mut config = ConfigSet::new();
1563 if let Some(v) = value {
1564 let _ = config.add_command_override("receive.denyCurrentBranch", v);
1565 }
1566 ReceivePolicy::from_config(&config).deny_current_branch
1567 };
1568 assert!(policy_for(None));
1569 assert!(!policy_for(Some("ignore")));
1570 assert!(!policy_for(Some("updateInstead")));
1571 assert!(policy_for(Some("refuse")));
1572 }
1573
1574 #[test]
1575 fn shallow_update_is_off_unless_configured() {
1576 assert!(!ReceivePolicy::default().shallow_update);
1577 let mut config = ConfigSet::new();
1578 let _ = config.add_command_override("receive.shallowUpdate", "true");
1579 assert!(ReceivePolicy::from_config(&config).shallow_update);
1580 }
1581
1582 #[test]
1583 fn deny_delete_current_values() {
1584 let policy_for = |value: Option<&str>| {
1585 let mut config = ConfigSet::new();
1586 if let Some(v) = value {
1587 let _ = config.add_command_override("receive.denyDeleteCurrent", v);
1588 }
1589 ReceivePolicy::from_config(&config).deny_delete_current
1590 };
1591 assert!(policy_for(None));
1592 assert!(!policy_for(Some("ignore")));
1593 assert!(!policy_for(Some("warn")));
1594 assert!(!policy_for(Some("false")));
1595 assert!(policy_for(Some("updateInstead")));
1597 assert!(policy_for(Some("refuse")));
1598 assert!(ReceivePolicy::default().deny_delete_current);
1599 }
1600}