1use std::collections::HashSet;
4use std::io::{BufReader, BufWriter, Read, Write};
5
6use super::{
7 advertised_refs, check_object_format, head_info, parse_oid, read_packet, write_flush,
8 write_line, Packet, Peel, ProtocolVersion, Result, ServeError, ServeOptions,
9};
10use crate::config::ConfigSet;
11use crate::hooks::{run_hook_in_git_dir, HookResult};
12use crate::index_pack::{install_pack_path, IngestPackOptions};
13use crate::objects::pack_stream_scan::{PackStreamScanError, PackStreamScanner};
14use crate::objects::ObjectId;
15use crate::odb::Odb;
16use crate::receive_pack::{max_input_size_from_config, should_use_unpack_objects};
17use crate::receive_quarantine::ReceiveQuarantine;
18use crate::repo::Repository;
19use crate::unpack_objects::{unpack_objects, UnpackOptions};
20
21const SPOOL_CHUNK: usize = 256 * 1024;
23
24const CAPABILITIES: &[&str] = &[
27 "report-status",
28 "report-status-v2",
29 "delete-refs",
30 "side-band-64k",
31 "quiet",
32 "atomic",
33 "ofs-delta",
34];
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct ReceivePolicy {
39 pub deny_non_fast_forwards: bool,
42 pub deny_deletes: bool,
44 pub deny_current_branch: bool,
47}
48
49impl Default for ReceivePolicy {
50 fn default() -> Self {
51 Self {
52 deny_non_fast_forwards: false,
53 deny_deletes: false,
54 deny_current_branch: true,
55 }
56 }
57}
58
59impl ReceivePolicy {
60 #[must_use]
74 pub fn from_config(config: &ConfigSet) -> Self {
75 let flag = |key: &str| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(false);
76 let deny_current_branch = match config.get("receive.denyCurrentBranch") {
77 None => true,
78 Some(v) => !matches!(
79 v.to_ascii_lowercase().as_str(),
80 "ignore" | "warn" | "false" | "no" | "off" | "0" | "updateinstead"
81 ),
82 };
83 Self {
84 deny_non_fast_forwards: flag("receive.denyNonFastForwards"),
85 deny_deletes: flag("receive.denyDeletes"),
86 deny_current_branch,
87 }
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct RefCommand {
94 pub refname: String,
96 pub old: Option<ObjectId>,
98 pub new: Option<ObjectId>,
100}
101
102impl RefCommand {
103 fn refused(&self, reason: RefUpdateRefusal) -> RefUpdateResult {
104 RefUpdateResult {
105 refname: self.refname.clone(),
106 old: self.old,
107 new: self.new,
108 error: Some(reason),
109 }
110 }
111
112 fn accepted(&self) -> RefUpdateResult {
113 RefUpdateResult {
114 refname: self.refname.clone(),
115 old: self.old,
116 new: self.new,
117 error: None,
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
128#[non_exhaustive]
129pub enum RefUpdateRefusal {
130 #[error("funny refname")]
132 InvalidRefName,
133 #[error("deny updating a hidden ref")]
135 HiddenRef,
136 #[error("stale info")]
138 StaleInfo,
139 #[error("deletion prohibited")]
141 DeletionProhibited,
142 #[error("deletion of the current branch prohibited")]
145 CurrentBranchDeletion,
146 #[error("branch is currently checked out")]
149 CurrentBranch,
150 #[error("missing necessary objects")]
152 MissingObjects,
153 #[error("non-fast-forward")]
155 NonFastForward,
156 #[error("bad ref")]
159 BadRef,
160 #[error("unable to verify fast-forward")]
164 FastForwardCheckFailed,
165 #[error("unpacker error")]
168 UnpackerError,
169 #[error("atomic transaction failed")]
172 AtomicTransactionFailed,
173 #[error("pre-receive hook declined")]
175 PreReceiveHookDeclined,
176 #[error("hook declined")]
178 UpdateHookDeclined,
179 #[error("{0}")]
181 Policy(String),
182 #[error("failed to update ref: {0}")]
184 RefStore(String),
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct RefUpdateResult {
190 pub refname: String,
192 pub old: Option<ObjectId>,
194 pub new: Option<ObjectId>,
196 pub error: Option<RefUpdateRefusal>,
198}
199
200#[derive(Debug, thiserror::Error)]
206#[non_exhaustive]
207pub enum UnpackError {
208 #[error("unexpected end of pack stream")]
210 Truncated,
211 #[error("pack exceeds maximum input size of {limit} bytes")]
213 TooLarge {
214 limit: u64,
216 },
217 #[error(transparent)]
219 Malformed(#[from] PackStreamScanError),
220 #[error(transparent)]
223 Ingest(Box<crate::error::Error>),
224 #[error(transparent)]
226 Io(#[from] std::io::Error),
227}
228
229impl From<crate::error::Error> for UnpackError {
230 fn from(err: crate::error::Error) -> Self {
231 Self::Ingest(Box::new(err))
232 }
233}
234
235pub trait ReceiveHook {
246 fn pre_receive(
262 &self,
263 commands: &[RefCommand],
264 odb: &Odb,
265 ) -> std::result::Result<(), RefUpdateRefusal> {
266 let _ = (commands, odb);
267 Ok(())
268 }
269
270 fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
285 let _ = (command, odb);
286 Ok(())
287 }
288}
289
290impl ReceiveHook for () {}
291
292impl<F> ReceiveHook for F
293where
294 F: Fn(&RefCommand, &Odb) -> std::result::Result<(), RefUpdateRefusal>,
295{
296 fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
297 self(command, odb)
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct HookRunRecord {
304 pub name: String,
306 pub ran: bool,
308 pub exit_code: Option<i32>,
310}
311
312#[derive(Debug, Default)]
314pub struct ReceiveOutcome {
315 pub unpack_error: Option<UnpackError>,
317 pub updates: Vec<RefUpdateResult>,
319 pub hooks: Vec<HookRunRecord>,
321 pub push_options: Vec<String>,
323}
324
325pub fn receive_pack(
334 repo: &Repository,
335 input: &mut dyn Read,
336 output: &mut dyn Write,
337 opts: &ServeOptions,
338 policy: &ReceivePolicy,
339) -> Result<ReceiveOutcome> {
340 receive_pack_with_hook(repo, input, output, opts, policy, &())
341}
342
343pub fn receive_pack_with_hook(
386 repo: &Repository,
387 input: &mut dyn Read,
388 output: &mut dyn Write,
389 opts: &ServeOptions,
390 policy: &ReceivePolicy,
391 hook: &dyn ReceiveHook,
392) -> Result<ReceiveOutcome> {
393 if opts.advertise_refs || !opts.stateless_rpc {
394 if opts.protocol == ProtocolVersion::V1 {
395 write_line(output, "version 1")?;
396 }
397 write_advertisement(repo, output, opts)?;
398 output.flush()?;
399 if opts.advertise_refs {
400 return Ok(ReceiveOutcome::default());
401 }
402 }
403
404 let Some(request) = read_commands(repo, input)? else {
405 return Ok(ReceiveOutcome::default());
406 };
407
408 let cfg = repo
409 .config()
410 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
411 let max_input = max_input_size_from_config(cfg.as_ref());
412 let sideband = request.caps.contains("side-band-64k");
413
414 let needs_pack = request.commands.iter().any(|c| c.new.is_some());
415 let mut quarantine = if needs_pack {
416 Some(ReceiveQuarantine::create(repo.odb(), repo.git_dir())?)
417 } else {
418 None
419 };
420
421 let unpack_error = match quarantine.as_ref() {
422 Some(q) => {
423 let odb = q.odb();
424 spool_push_pack(input, &odb, max_input)
425 .and_then(|pack| ingest_spooled_pack(&odb, pack, cfg.as_ref()))
426 .err()
427 }
428 None => None,
429 };
430
431 let mut hook_records = Vec::new();
432 let updates = if unpack_error.is_some() {
433 request
434 .commands
435 .iter()
436 .map(|c| c.refused(RefUpdateRefusal::UnpackerError))
437 .collect()
438 } else {
439 apply_commands_with_hooks(
440 repo,
441 &request,
442 opts,
443 policy,
444 hook,
445 quarantine.as_mut(),
446 sideband.then_some(&mut *output),
447 &mut hook_records,
448 )?
449 };
450
451 if request.caps.contains("report-status") || request.caps.contains("report-status-v2") {
452 let report = build_report(unpack_error.as_ref(), &updates)?;
453 if sideband {
454 crate::pkt_line::write_sideband_channel1_64k(&mut &mut *output, &report)?;
455 write_flush(output)?;
456 } else {
457 output.write_all(&report)?;
458 }
459 }
460 output.flush()?;
461
462 Ok(ReceiveOutcome {
463 unpack_error,
464 updates,
465 hooks: hook_records,
466 push_options: request.push_options.clone(),
467 })
468}
469
470fn write_advertisement(
472 repo: &Repository,
473 output: &mut dyn Write,
474 opts: &ServeOptions,
475) -> Result<()> {
476 let mut caps: Vec<String> = CAPABILITIES.iter().map(|c| (*c).to_owned()).collect();
477 let config = repo
478 .config()
479 .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
480 if config
481 .get_bool("receive.advertisepushoptions")
482 .and_then(|r| r.ok())
483 .unwrap_or(false)
484 {
485 caps.push("push-options".to_owned());
486 }
487 caps.push(format!("object-format={}", repo.odb().hash_algo().name()));
488 if !opts.agent.is_empty() {
489 caps.push(format!("agent={}", opts.agent));
490 }
491 let caps = caps.join(" ");
492
493 let refs = advertised_refs(repo, &opts.hidden_refs, &[], Peel::No)?;
494 if refs.is_empty() {
495 let zero = ObjectId::null(repo.odb().hash_algo());
496 write_line(
497 output,
498 &format!("{} capabilities^{{}}\0{caps}", zero.to_hex()),
499 )?;
500 }
501 for (i, r) in refs.iter().enumerate() {
502 if i == 0 {
503 write_line(output, &format!("{} {}\0{caps}", r.oid.to_hex(), r.name))?;
504 } else {
505 write_line(output, &format!("{} {}", r.oid.to_hex(), r.name))?;
506 }
507 }
508 write_flush(output)
509}
510
511struct PushRequest {
513 commands: Vec<RefCommand>,
514 caps: HashSet<String>,
515 push_options: Vec<String>,
516}
517
518fn read_commands(repo: &Repository, input: &mut dyn Read) -> Result<Option<PushRequest>> {
521 let mut commands = Vec::new();
522 let mut caps = HashSet::new();
523 loop {
524 match read_packet(input)? {
525 None | Some(Packet::Flush) => break,
526 Some(Packet::Data(line)) => {
527 let (line, line_caps) = line.split_once('\0').unwrap_or((line.as_str(), ""));
528 if commands.is_empty() {
529 caps.extend(
530 line_caps
531 .split(' ')
532 .filter(|c| !c.is_empty())
533 .map(str::to_owned),
534 );
535 }
536 let mut parts = line.splitn(3, ' ');
537 let (Some(old), Some(new), Some(refname)) =
538 (parts.next(), parts.next(), parts.next())
539 else {
540 return Err(ServeError::Protocol(format!("malformed command '{line}'")));
541 };
542 let old = parse_oid(repo, old)?;
543 let new = parse_oid(repo, new)?;
544 commands.push(RefCommand {
545 old: (!old.is_zero()).then_some(old),
546 new: (!new.is_zero()).then_some(new),
547 refname: refname.to_owned(),
548 });
549 }
550 Some(other) => {
551 return Err(ServeError::Protocol(format!(
552 "unexpected packet in command list: {other:?}"
553 )))
554 }
555 }
556 }
557 if commands.is_empty() {
558 return Ok(None);
559 }
560 let mut push_options = Vec::new();
561 if caps.contains("push-options") {
562 loop {
563 match read_packet(input)? {
564 None | Some(Packet::Flush) => break,
565 Some(Packet::Data(line)) => {
566 push_options.push(line);
567 }
568 Some(other) => {
569 return Err(ServeError::Protocol(format!(
570 "unexpected packet in push options: {other:?}"
571 )))
572 }
573 }
574 }
575 }
576 let format = caps.iter().find_map(|c| c.strip_prefix("object-format="));
577 check_object_format(repo, format)?;
578 Ok(Some(PushRequest {
579 commands,
580 caps,
581 push_options,
582 }))
583}
584
585#[allow(clippy::too_many_arguments)]
587fn apply_commands_with_hooks(
588 repo: &Repository,
589 request: &PushRequest,
590 opts: &ServeOptions,
591 policy: &ReceivePolicy,
592 hook: &dyn ReceiveHook,
593 quarantine: Option<&mut ReceiveQuarantine>,
594 mut sideband_out: Option<&mut dyn Write>,
595 hook_records: &mut Vec<HookRunRecord>,
596) -> Result<Vec<RefUpdateResult>> {
597 let lookup_odb = quarantine.as_ref().map(|q| q.odb());
598 let current_branch = if repo.is_bare() {
599 None
600 } else {
601 head_info(repo).target
602 };
603 let mut results: Vec<RefUpdateResult> = request
604 .commands
605 .iter()
606 .map(|c| {
607 Ok(
608 match check_command(
609 repo,
610 lookup_odb.as_ref(),
611 c,
612 opts,
613 policy,
614 current_branch.as_deref(),
615 )? {
616 Some(reason) => c.refused(reason),
617 None => c.accepted(),
618 },
619 )
620 })
621 .collect::<Result<_>>()?;
622
623 let atomic = request.caps.contains("atomic");
624 if atomic && results.iter().any(|r| r.error.is_some()) {
625 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
626 return Ok(results);
627 }
628
629 let accepted: Vec<RefCommand> = request
630 .commands
631 .iter()
632 .zip(&results)
633 .filter(|(_, r)| r.error.is_none())
634 .map(|(c, _)| c.clone())
635 .collect();
636 if !accepted.is_empty() {
637 if let Err(refusal) = hook.pre_receive(&accepted, lookup_odb.as_ref().unwrap_or(repo.odb()))
638 {
639 refuse_remaining(&mut results, &refusal);
640 return Ok(results);
641 }
642 }
643
644 let algo = repo.odb().hash_algo();
645 let push_option_env = push_option_env_owned(&request.push_options);
646 let mut pre_hook_env_owned = push_option_env.clone();
647 if let Some(q) = quarantine.as_ref() {
648 pre_hook_env_owned.extend(q.hook_env());
649 }
650 let pre_hook_env: Vec<(&str, &str)> = pre_hook_env_owned
651 .iter()
652 .map(|(k, v)| (k.as_str(), v.as_str()))
653 .collect();
654 let update_hook_env: &[(&str, &str)] = &[];
655 let post_receive_env_owned = push_option_env;
656 let post_receive_env: Vec<(&str, &str)> = post_receive_env_owned
657 .iter()
658 .map(|(k, v)| (k.as_str(), v.as_str()))
659 .collect();
660
661 let all_cmds: Vec<&RefCommand> = request.commands.iter().collect();
662 let pre_stdin = commands_stdin(&all_cmds, algo);
663 let pre_result = run_hook_in_git_dir(
664 repo,
665 "pre-receive",
666 &[],
667 Some(pre_stdin.as_bytes()),
668 &pre_hook_env,
669 );
670 relay_hook_sideband(&mut sideband_out, &pre_result)?;
671 hook_records.push(hook_record("pre-receive", &pre_result));
672 if !pre_result.is_ok() {
673 refuse_remaining(&mut results, &RefUpdateRefusal::PreReceiveHookDeclined);
674 return Ok(results);
675 }
676
677 if results.iter().all(|r| r.error.is_some()) {
678 return Ok(results);
679 }
680
681 if let Some(q) = quarantine {
682 q.migrate().map_err(ServeError::Repository)?;
683 repo.odb().invalidate_packs();
684 }
685
686 for (cmd, result) in request.commands.iter().zip(results.iter_mut()) {
687 if result.error.is_some() {
688 continue;
689 }
690 if let Err(refusal) = hook.update(cmd, repo.odb()) {
691 result.error = Some(refusal);
692 continue;
693 }
694 let zero = null_oid_hex(algo);
695 let old_hex = cmd.old.map(|o| o.to_hex()).unwrap_or_else(|| zero.clone());
696 let new_hex = cmd.new.map(|o| o.to_hex()).unwrap_or(zero.clone());
697 let args = [cmd.refname.as_str(), old_hex.as_str(), new_hex.as_str()];
698 let update_result = run_hook_in_git_dir(repo, "update", &args, None, update_hook_env);
699 relay_hook_sideband(&mut sideband_out, &update_result)?;
700 hook_records.push(hook_record("update", &update_result));
701 if !update_result.is_ok() {
702 result.error = Some(RefUpdateRefusal::UpdateHookDeclined);
703 }
704 }
705
706 if atomic && results.iter().any(|r| r.error.is_some()) {
707 refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
708 return Ok(results);
709 }
710
711 let pending: Vec<(usize, crate::refs::store::RefUpdate)> = request
712 .commands
713 .iter()
714 .zip(results.iter())
715 .enumerate()
716 .filter(|(_, (_, r))| r.error.is_none())
717 .map(|(idx, (cmd, _))| (idx, receive_command_store_update(cmd)))
718 .collect();
719
720 if atomic {
721 let batch: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
722 if let Err(e) = crate::refs::commit_ref_store_batch(repo.git_dir(), &batch) {
723 let refusal = RefUpdateRefusal::RefStore(e.to_string());
724 for (idx, _) in pending {
725 if results[idx].error.is_none() {
726 results[idx].error = Some(refusal.clone());
727 }
728 }
729 }
730 } else {
731 for (idx, update) in pending {
732 if let Err(e) = crate::refs::commit_store_update(repo.git_dir(), update) {
733 results[idx].error = Some(RefUpdateRefusal::RefStore(e.to_string()));
734 }
735 }
736 }
737
738 let successful: Vec<&RefCommand> = request
739 .commands
740 .iter()
741 .zip(results.iter())
742 .filter(|(_, r)| r.error.is_none())
743 .map(|(c, _)| c)
744 .collect();
745 if !successful.is_empty() {
746 let post_stdin = commands_stdin(&successful, algo);
747 let post_result = run_hook_in_git_dir(
748 repo,
749 "post-receive",
750 &[],
751 Some(post_stdin.as_bytes()),
752 &post_receive_env,
753 );
754 relay_hook_sideband(&mut sideband_out, &post_result)?;
755 hook_records.push(hook_record("post-receive", &post_result));
756
757 let mut post_update_args: Vec<&str> = Vec::new();
758 for result in results.iter().filter(|r| r.error.is_none()) {
759 post_update_args.push(result.refname.as_str());
760 }
761 let pu_result = run_hook_in_git_dir(
762 repo,
763 "post-update",
764 &post_update_args,
765 None,
766 update_hook_env,
767 );
768 relay_hook_sideband(&mut sideband_out, &pu_result)?;
769 hook_records.push(hook_record("post-update", &pu_result));
770 }
771
772 Ok(results)
773}
774
775fn refuse_remaining(results: &mut [RefUpdateResult], reason: &RefUpdateRefusal) {
777 for r in results.iter_mut().filter(|r| r.error.is_none()) {
778 r.error = Some(reason.clone());
779 }
780}
781
782fn hook_record(name: &str, result: &HookResult) -> HookRunRecord {
783 HookRunRecord {
784 name: name.to_owned(),
785 ran: result.was_executed(),
786 exit_code: result.status(),
787 }
788}
789
790fn push_option_env_owned(options: &[String]) -> Vec<(String, String)> {
791 let mut pairs = vec![(
792 "GIT_PUSH_OPTION_COUNT".to_owned(),
793 options.len().to_string(),
794 )];
795 for (i, opt) in options.iter().enumerate() {
796 pairs.push((format!("GIT_PUSH_OPTION_{i}"), opt.clone()));
797 }
798 pairs
799}
800
801fn commands_stdin(commands: &[&RefCommand], algo: crate::objects::HashAlgo) -> String {
802 let zero = null_oid_hex(algo);
803 commands
804 .iter()
805 .map(|c| {
806 format!(
807 "{} {} {}\n",
808 c.old.map(|o| o.to_hex()).unwrap_or_else(|| zero.clone()),
809 c.new.map(|o| o.to_hex()).unwrap_or_else(|| zero.clone()),
810 c.refname
811 )
812 })
813 .collect()
814}
815
816fn null_oid_hex(algo: crate::objects::HashAlgo) -> String {
817 ObjectId::null(algo).to_hex()
818}
819
820fn receive_command_store_update(cmd: &RefCommand) -> crate::refs::store::RefUpdate {
821 let mut flags = crate::refs::store::RefUpdateFlags::default();
822 if cmd.refname == "HEAD" {
823 flags.no_deref = true;
824 }
825 crate::refs::store::RefUpdate {
826 name: cmd.refname.clone(),
827 new_value: cmd.new.map(crate::refs::store::RawRef::Direct),
828 expected: match cmd.old {
829 Some(oid) => crate::refs::store::Expected::Oid(oid),
830 None => crate::refs::store::Expected::Missing,
831 },
832 reflog: None,
833 flags,
834 }
835}
836
837fn relay_hook_sideband(out: &mut Option<&mut dyn Write>, result: &HookResult) -> Result<()> {
839 let (Some(w), Some(output)) = (out.as_mut(), result.output()) else {
840 return Ok(());
841 };
842 for chunk in output
843 .stdout
844 .chunks(65515)
845 .chain(output.stderr.chunks(65515))
846 {
847 crate::pkt_line::write_sideband_packet(*w, 2, chunk).map_err(ServeError::Io)?;
848 }
849 Ok(())
850}
851
852fn check_command(
859 repo: &Repository,
860 lookup: Option<&Odb>,
861 cmd: &RefCommand,
862 opts: &ServeOptions,
863 policy: &ReceivePolicy,
864 current_branch: Option<&str>,
865) -> Result<Option<RefUpdateRefusal>> {
866 if !cmd.refname.starts_with("refs/") || !crate::refs::is_valid_storable_ref_name(&cmd.refname) {
867 return Ok(Some(RefUpdateRefusal::InvalidRefName));
868 }
869 if crate::hide_refs::ref_is_hidden(&cmd.refname, &cmd.refname, &opts.hidden_refs) {
870 return Ok(Some(RefUpdateRefusal::HiddenRef));
871 }
872 let current = crate::refs::resolve_ref(repo.git_dir(), &cmd.refname).ok();
873 if current != cmd.old {
874 return Ok(Some(RefUpdateRefusal::StaleInfo));
875 }
876 let is_current_branch = current_branch == Some(cmd.refname.as_str());
877
878 let Some(new) = cmd.new else {
879 if policy.deny_deletes {
880 return Ok(Some(RefUpdateRefusal::DeletionProhibited));
881 }
882 if is_current_branch && policy.deny_current_branch {
883 return Ok(Some(RefUpdateRefusal::CurrentBranchDeletion));
884 }
885 return Ok(None);
886 };
887 let odb = lookup.unwrap_or(repo.odb());
888 if !odb.contains(&new)? {
889 return Ok(Some(RefUpdateRefusal::MissingObjects));
890 }
891 if is_current_branch && policy.deny_current_branch {
892 return Ok(Some(RefUpdateRefusal::CurrentBranch));
893 }
894 if let (Some(old), true) = (current, policy.deny_non_fast_forwards) {
895 if cmd.refname.starts_with("refs/heads/") {
896 return Ok(check_fast_forward(repo, odb, old, new));
897 }
898 }
899 Ok(None)
900}
901
902fn check_fast_forward(
907 repo: &Repository,
908 odb: &crate::odb::Odb,
909 old: ObjectId,
910 new: ObjectId,
911) -> Option<RefUpdateRefusal> {
912 let is_commit = |oid: &ObjectId| {
913 odb.read_info(oid)
914 .is_ok_and(|info| info.kind == crate::objects::ObjectKind::Commit)
915 };
916 if !is_commit(&old) || !is_commit(&new) {
917 return Some(RefUpdateRefusal::BadRef);
918 }
919 let graph = repo.caches().commit_graph_chain(repo.odb().objects_dir());
920 match crate::merge_base::is_ancestor_odb_walk_with_graph(odb, graph.as_deref(), old, new) {
921 Ok(true) => None,
922 Ok(false) => Some(RefUpdateRefusal::NonFastForward),
923 Err(_) => Some(RefUpdateRefusal::FastForwardCheckFailed),
924 }
925}
926
927fn build_report(
930 unpack_error: Option<&UnpackError>,
931 updates: &[RefUpdateResult],
932) -> Result<Vec<u8>> {
933 let mut report = Vec::new();
934 match unpack_error {
935 None => write_line(&mut report, "unpack ok")?,
936 Some(e) => write_line(&mut report, &format!("unpack {}", one_line(&e.to_string())))?,
937 }
938 for u in updates {
939 match &u.error {
940 None => write_line(&mut report, &format!("ok {}", u.refname))?,
941 Some(reason) => write_line(
942 &mut report,
943 &format!("ng {} {}", u.refname, one_line(&reason.to_string())),
944 )?,
945 }
946 }
947 write_flush(&mut report)?;
948 Ok(report)
949}
950
951fn one_line(msg: &str) -> String {
953 msg.lines().next().unwrap_or_default().trim().to_owned()
954}
955
956struct SpooledPack {
958 file: tempfile::NamedTempFile,
959 header: [u8; 12],
961}
962
963fn spool_push_pack(
971 input: &mut dyn Read,
972 odb: &Odb,
973 max_bytes: Option<u64>,
974) -> std::result::Result<SpooledPack, UnpackError> {
975 let hash = odb.hash_algo();
976 let mut file = tempfile::Builder::new()
977 .prefix("tmp_pack_")
978 .tempfile_in(odb.objects_dir().join("pack"))?;
979 let mut scanner = PackStreamScanner::new(hash);
980 let mut header = [0u8; 12];
981 let mut chunk = vec![0u8; SPOOL_CHUNK];
982 let mut written = 0u64;
983 let mut pack_len: Option<u64> = None;
984 let mut out = BufWriter::with_capacity(SPOOL_CHUNK, file.as_file_mut());
985 loop {
986 let want = match pack_len {
987 Some(len) if written >= len => break,
988 Some(len) => usize::try_from(len - written).map_or(chunk.len(), |n| n.min(chunk.len())),
989 None => chunk.len(),
990 };
991 let n = read_some(input, &mut chunk[..want])?;
992 if n == 0 {
993 return Err(UnpackError::Truncated);
994 }
995 let mut data = &chunk[..n];
996 if pack_len.is_none() {
997 scanner.feed(data)?;
998 if scanner.is_complete() {
999 let len = scanner
1000 .pack_body_byte_len()
1001 .saturating_add(hash.len() as u64);
1002 let keep = usize::try_from(len.saturating_sub(written)).map_or(n, |k| k.min(n));
1003 data = &data[..keep];
1004 pack_len = Some(len);
1005 }
1006 }
1007 if let Some(limit) = max_bytes {
1008 let total = pack_len.unwrap_or(written.saturating_add(data.len() as u64));
1009 if total > limit {
1010 return Err(UnpackError::TooLarge { limit });
1011 }
1012 }
1013 if let Some(missing) = usize::try_from(written)
1014 .ok()
1015 .and_then(|start| header.get_mut(start..))
1016 {
1017 let take = missing.len().min(data.len());
1018 missing[..take].copy_from_slice(&data[..take]);
1019 }
1020 out.write_all(data)?;
1021 written += data.len() as u64;
1022 }
1023 out.flush()?;
1024 drop(out);
1025 Ok(SpooledPack { file, header })
1026}
1027
1028fn read_some(input: &mut dyn Read, buf: &mut [u8]) -> std::io::Result<usize> {
1030 loop {
1031 match input.read(buf) {
1032 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1033 other => return other,
1034 }
1035 }
1036}
1037
1038fn ingest_spooled_pack(
1041 odb: &Odb,
1042 pack: SpooledPack,
1043 cfg: &ConfigSet,
1044) -> std::result::Result<(), UnpackError> {
1045 if should_use_unpack_objects(&pack.header, cfg) {
1046 let mut reader = BufReader::with_capacity(SPOOL_CHUNK, pack.file.reopen()?);
1047 unpack_objects(
1048 &mut reader,
1049 odb,
1050 &UnpackOptions {
1051 quiet: true,
1052 strict: true,
1053 ..UnpackOptions::default()
1054 },
1055 )?;
1056 return Ok(());
1057 }
1058 let path = pack.file.into_temp_path();
1059 install_pack_path(
1060 &path,
1061 odb,
1062 &IngestPackOptions {
1063 fix_thin: true,
1064 skip_post_index_verify: true,
1067 ..Default::default()
1068 },
1069 )?;
1070 Ok(())
1071}
1072
1073#[cfg(test)]
1074mod tests {
1075 use super::*;
1076
1077 #[test]
1078 fn report_lists_unpack_status_then_each_ref() {
1079 let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
1080 .unwrap_or_else(|_| ObjectId::zero());
1081 let updates = vec![
1082 RefUpdateResult {
1083 refname: "refs/heads/main".into(),
1084 old: None,
1085 new: Some(oid),
1086 error: None,
1087 },
1088 RefUpdateResult {
1089 refname: "refs/heads/topic".into(),
1090 old: Some(oid),
1091 new: None,
1092 error: Some(RefUpdateRefusal::DeletionProhibited),
1093 },
1094 ];
1095 let report = build_report(None, &updates).unwrap_or_default();
1096 assert_eq!(
1097 String::from_utf8_lossy(&report),
1098 "000eunpack ok\n0017ok refs/heads/main\n002cng refs/heads/topic deletion prohibited\n0000"
1099 );
1100 }
1101
1102 #[test]
1103 fn report_renders_typed_unpack_error_and_refusal() {
1104 let updates = vec![RefUpdateResult {
1105 refname: "refs/heads/main".into(),
1106 old: None,
1107 new: None,
1108 error: Some(RefUpdateRefusal::UnpackerError),
1109 }];
1110 let report = build_report(Some(&UnpackError::Truncated), &updates).unwrap_or_default();
1111 assert_eq!(
1112 String::from_utf8_lossy(&report),
1113 "0029unpack unexpected end of pack stream\n0026ng refs/heads/main unpacker error\n0000"
1114 );
1115 }
1116
1117 #[test]
1118 fn refusals_render_the_reasons_git_clients_expect() {
1119 let cases = [
1120 (RefUpdateRefusal::InvalidRefName, "funny refname"),
1121 (RefUpdateRefusal::HiddenRef, "deny updating a hidden ref"),
1122 (RefUpdateRefusal::StaleInfo, "stale info"),
1123 (RefUpdateRefusal::DeletionProhibited, "deletion prohibited"),
1124 (
1125 RefUpdateRefusal::CurrentBranchDeletion,
1126 "deletion of the current branch prohibited",
1127 ),
1128 (
1129 RefUpdateRefusal::CurrentBranch,
1130 "branch is currently checked out",
1131 ),
1132 (
1133 RefUpdateRefusal::MissingObjects,
1134 "missing necessary objects",
1135 ),
1136 (RefUpdateRefusal::NonFastForward, "non-fast-forward"),
1137 (RefUpdateRefusal::BadRef, "bad ref"),
1138 (
1139 RefUpdateRefusal::FastForwardCheckFailed,
1140 "unable to verify fast-forward",
1141 ),
1142 (RefUpdateRefusal::UnpackerError, "unpacker error"),
1143 (
1144 RefUpdateRefusal::AtomicTransactionFailed,
1145 "atomic transaction failed",
1146 ),
1147 (
1148 RefUpdateRefusal::PreReceiveHookDeclined,
1149 "pre-receive hook declined",
1150 ),
1151 (RefUpdateRefusal::UpdateHookDeclined, "hook declined"),
1152 (RefUpdateRefusal::Policy("frozen".into()), "frozen"),
1153 (
1154 RefUpdateRefusal::RefStore("locked".into()),
1155 "failed to update ref: locked",
1156 ),
1157 ];
1158 for (refusal, wire) in cases {
1159 assert_eq!(refusal.to_string(), wire);
1160 }
1161 }
1162
1163 struct NoReadPastEnd<'a> {
1166 data: &'a [u8],
1167 }
1168
1169 impl Read for NoReadPastEnd<'_> {
1170 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1171 if self.data.is_empty() {
1172 return Err(std::io::Error::other("read past the end of the pack"));
1173 }
1174 let n = buf.len().min(self.data.len());
1175 buf[..n].copy_from_slice(&self.data[..n]);
1176 self.data = &self.data[n..];
1177 Ok(n)
1178 }
1179 }
1180
1181 fn sample_pack() -> Vec<u8> {
1182 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
1183 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
1184 let mut seed = 0x2545_f491_u32;
1185 let big: Vec<u8> = (0..(SPOOL_CHUNK * 3))
1187 .map(|_| {
1188 seed ^= seed << 13;
1189 seed ^= seed >> 17;
1190 seed ^= seed << 5;
1191 seed as u8
1192 })
1193 .collect();
1194 pb.add_full(PK::Blob, &big);
1195 pb.add_full(PK::Blob, b"small\n");
1196 pb.build().bytes
1197 }
1198
1199 fn test_odb(dir: &std::path::Path) -> Odb {
1200 let objects = dir.join("objects");
1201 std::fs::create_dir_all(objects.join("pack")).unwrap_or_default();
1202 Odb::new(&objects)
1203 }
1204
1205 #[test]
1206 fn spool_stops_at_the_trailer_without_reading_further() {
1207 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1208 let odb = test_odb(tmp.path());
1209 let pack = sample_pack();
1210 let mut input = NoReadPastEnd { data: &pack };
1211 let spooled = spool_push_pack(&mut input, &odb, None).unwrap_or_else(|e| panic!("{e}"));
1212 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1213 assert_eq!(&spooled.header[..], &pack[..12]);
1214 }
1215
1216 #[test]
1217 fn spool_keeps_only_the_pack_when_more_bytes_follow() {
1218 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1219 let odb = test_odb(tmp.path());
1220 let pack = sample_pack();
1221 let mut stream = pack.clone();
1222 stream.extend_from_slice(b"trailing junk");
1223 let spooled =
1224 spool_push_pack(&mut stream.as_slice(), &odb, None).unwrap_or_else(|e| panic!("{e}"));
1225 assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1226 }
1227
1228 #[test]
1229 fn spool_reports_typed_errors() {
1230 let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1231 let odb = test_odb(tmp.path());
1232 let pack = sample_pack();
1233
1234 let truncated = &pack[..pack.len() - 5];
1235 assert!(matches!(
1236 spool_push_pack(&mut &truncated[..], &odb, None),
1237 Err(UnpackError::Truncated)
1238 ));
1239
1240 let limit = (pack.len() - 1) as u64;
1241 assert!(matches!(
1242 spool_push_pack(&mut pack.as_slice(), &odb, Some(limit)),
1243 Err(UnpackError::TooLarge { limit: l }) if l == limit
1244 ));
1245 assert!(spool_push_pack(&mut pack.as_slice(), &odb, Some(pack.len() as u64)).is_ok());
1246
1247 assert!(matches!(
1248 spool_push_pack(&mut &b"JUNK\0\0\0\x02\0\0\0\x01"[..], &odb, None),
1249 Err(UnpackError::Malformed(PackStreamScanError::BadSignature))
1250 ));
1251
1252 let leftovers: Vec<_> = std::fs::read_dir(odb.objects_dir().join("pack"))
1253 .map(|rd| rd.filter_map(|e| e.ok()).collect())
1254 .unwrap_or_default();
1255 assert!(
1256 leftovers.is_empty(),
1257 "failed spools must not leave temp files"
1258 );
1259 }
1260
1261 #[test]
1262 fn deny_current_branch_values() {
1263 let policy_for = |value: Option<&str>| {
1264 let mut config = ConfigSet::new();
1265 if let Some(v) = value {
1266 let _ = config.add_command_override("receive.denyCurrentBranch", v);
1267 }
1268 ReceivePolicy::from_config(&config).deny_current_branch
1269 };
1270 assert!(policy_for(None));
1271 assert!(!policy_for(Some("ignore")));
1272 assert!(!policy_for(Some("updateInstead")));
1273 assert!(policy_for(Some("refuse")));
1274 }
1275}