1mod connectivity;
21mod object_info;
22pub(crate) mod promised_objects;
23pub(crate) mod push_to_checkout;
24mod receive_pack;
25mod ref_update_policy;
26mod shallow_upload;
27mod upload_pack;
28mod upload_pack_policy;
29
30pub(crate) use receive_pack::ReceiveHookScripts;
31pub(crate) use shallow_upload::{compute_shallow_response, ShallowRequest};
32
33pub use receive_pack::{
34 receive_pack, receive_pack_with_hook, receive_pack_with_hook_cancellable, ReceiveHook,
35 ReceiveHookContext, ReceiveOutcome, ReceivePolicy, RefCommand, RefUpdateRefusal,
36 RefUpdateResult, UnpackError,
37};
38pub(crate) use ref_update_policy::{checked_out_branch, validate_ref_update};
39pub(crate) use ref_update_policy::{head_branch, HeadBranches};
40pub use upload_pack::{upload_pack, upload_pack_cancellable, UploadOutcome};
41
42use std::cell::RefCell;
43use std::collections::HashMap;
44use std::ops::ControlFlow;
45
46use crate::objects::ObjectId;
47use crate::refs::store::RawRef;
48use crate::refs::PackedRefPeels;
49use crate::repo::Repository;
50
51#[derive(Debug, thiserror::Error)]
53pub enum ServeError {
54 #[error("protocol error: {0}")]
56 Protocol(String),
57 #[error("not our ref: {0}")]
59 NotOurRef(ObjectId),
60 #[error("object {oid} is missing: {reason}")]
67 ObjectMissing {
68 oid: ObjectId,
70 reason: MissingObjectReason,
72 },
73 #[error("unknown command: {0}")]
75 UnknownCommand(String),
76 #[error("invalid deepen-not '{spec}': {reason}")]
79 InvalidDeepenNot {
80 spec: String,
82 reason: DeepenNotError,
84 },
85 #[error(transparent)]
87 UploadFilter(#[from] crate::upload_filter::UploadFilterError),
88 #[error("object format mismatch: client wants {client}, repository uses {repository}")]
90 ObjectFormatMismatch {
91 client: String,
93 repository: &'static str,
95 },
96 #[error(transparent)]
98 Repository(#[from] crate::error::Error),
99 #[error(transparent)]
101 Io(#[from] std::io::Error),
102 #[error("cancelled")]
104 Cancelled,
105 #[error("the pack failed partway through: {0}")]
110 PackInterrupted(Box<ServeError>),
111}
112
113#[derive(Debug)]
119pub struct RequestBodyLimitExceeded;
120
121impl std::fmt::Display for RequestBodyLimitExceeded {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 write!(f, "fetch request body exceeds the decoded size limit")
124 }
125}
126
127impl std::error::Error for RequestBodyLimitExceeded {}
128
129#[must_use]
131pub fn io_error_is_request_body_limit(err: &std::io::Error) -> bool {
132 err.get_ref()
133 .is_some_and(|inner| inner.is::<RequestBodyLimitExceeded>())
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
145#[non_exhaustive]
146pub enum MissingObjectReason {
147 #[error("it is stored on a promisor remote the client did not accept")]
150 Promised,
151 #[error("no promisor remote has it either")]
154 NotPromised,
155 #[error("lazy fetch is on but no promisor fetcher is installed")]
158 NoFetcher,
159 #[error("fetching it from the promisor remotes did not bring it in")]
161 LazyFetchFailed,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
166pub enum DeepenNotError {
167 #[error("no such ref or object")]
169 UnknownRef,
170 #[error("matches more than one ref")]
172 AmbiguousRef,
173 #[error("does not point at a commit")]
175 NotACommit,
176}
177
178pub type Result<T> = std::result::Result<T, ServeError>;
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
187pub enum ProtocolVersion {
188 #[default]
190 V0,
191 V1,
193 V2,
195}
196
197impl ProtocolVersion {
198 #[must_use]
212 pub fn from_git_protocol(value: &str) -> Self {
213 value
214 .split(':')
215 .filter_map(|entry| entry.strip_prefix("version="))
216 .filter_map(|v| match v {
217 "2" => Some(Self::V2),
218 "1" => Some(Self::V1),
219 "0" => Some(Self::V0),
220 _ => None,
221 })
222 .max_by_key(|v| match v {
223 Self::V0 => 0,
224 Self::V1 => 1,
225 Self::V2 => 2,
226 })
227 .unwrap_or_default()
228 }
229}
230
231#[derive(Debug, Clone, Default)]
233pub struct ServeOptions {
234 pub protocol: ProtocolVersion,
236 pub stateless_rpc: bool,
239 pub advertise_refs: bool,
242 pub agent: String,
244 pub hidden_refs: Vec<String>,
247 pub server_options: Vec<String>,
251 pub bundle_list: Option<crate::bundle_uri::BundleList>,
254 pub push_bundle: Option<String>,
258 pub accepted_promisor_remotes: Vec<String>,
267 pub lazy_fetch: Option<bool>,
279}
280
281impl ServeOptions {
282 #[must_use]
295 pub fn lazy_fetch_from_environment(env: &crate::environment::Environment) -> Option<bool> {
296 [
297 crate::promisor_fetch::NO_LAZY_FETCH_ENV,
298 "GIT_NO_LAZY_FETCH",
299 ]
300 .into_iter()
301 .find_map(|key| env.var(key))
302 .and_then(|value| crate::config::parse_bool(value.trim()).ok())
303 .map(|no_lazy_fetch| !no_lazy_fetch)
304 }
305
306 #[must_use]
317 pub fn lazy_fetch_enabled(&self, config: &crate::config::ConfigSet) -> bool {
318 self.lazy_fetch.unwrap_or_else(|| {
319 config
320 .get_bool("uploadpack.lazyfetch")
321 .and_then(|v| v.ok())
322 .unwrap_or(false)
323 })
324 }
325
326 pub fn attach_promisor_fetcher(
345 &self,
346 repo: &Repository,
347 config: &crate::config::ConfigSet,
348 fetcher: Option<std::sync::Arc<dyn crate::promisor_fetch::PromisorFetcher>>,
349 ) -> bool {
350 let fetcher = fetcher.filter(|_| self.lazy_fetch_enabled(config));
351 let attached = fetcher.is_some();
352 repo.caches().set_promisor_fetcher(fetcher);
353 attached
354 }
355}
356
357pub(crate) const PUSH_BUNDLE_CAPABILITY: &str = "pushBundle";
360
361#[cfg(test)]
363#[derive(Debug, Clone)]
364pub(crate) struct AdvertisedRef {
365 pub(crate) name: String,
367 pub(crate) oid: ObjectId,
369 pub(crate) peeled: Option<ObjectId>,
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375pub(crate) enum Peel {
376 No,
378 Yes,
380}
381
382#[derive(Debug, Clone, Copy)]
384pub(crate) struct AdvertisedRefView<'a> {
385 pub(crate) name: &'a str,
387 pub(crate) oid: ObjectId,
389 pub(crate) peeled: Option<ObjectId>,
391}
392
393pub(crate) fn for_each_advertised_ref(
406 repo: &Repository,
407 hidden: &[String],
408 prefixes: &[&str],
409 peel: Peel,
410 mut f: impl FnMut(AdvertisedRefView<'_>) -> Result<ControlFlow<()>>,
411) -> Result<()> {
412 let store = repo.refs();
413 let git_dir = repo.git_dir();
414 let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
415 let packed = (peel == Peel::Yes)
416 .then(|| PackedRefPeels::for_store(store, git_dir).ok())
417 .flatten();
418 let odb = repo.odb();
419 let mut peel_cache: HashMap<ObjectId, Option<ObjectId>> = HashMap::new();
420 let callback_err = RefCell::new(None::<Result<()>>);
421 let peel_ctx = peel == Peel::Yes;
422 let run = || {
423 for prefix in scan_prefixes(prefixes) {
424 store.for_each_ref(prefix, &mut |entry| {
425 if callback_err.borrow().is_some() {
426 return ControlFlow::Break(());
427 }
428 let name = entry.name.as_str();
429 if !name.starts_with(prefix)
430 || !crate::refs::is_valid_fetch_advertised_ref(name)
431 || crate::hide_refs::ref_is_hidden(name, name, hidden)
432 || (linked_worktree
433 && !crate::worktree_ref::ref_visible_from_worktree(git_dir, name))
434 {
435 return ControlFlow::Continue(());
436 }
437 let oid = match &entry.value {
438 RawRef::Direct(oid) => Some(*oid),
439 RawRef::Symbolic(_) => entry.peeled.or_else(|| store.resolve(name).ok()),
440 };
441 let Some(oid) = oid else {
442 return ControlFlow::Continue(());
443 };
444 let entry_peeled = match &entry.value {
445 RawRef::Direct(_) => entry.peeled,
446 RawRef::Symbolic(_) => None,
447 };
448 let peeled: Option<ObjectId> = match peel {
449 Peel::No => None,
450 Peel::Yes => {
451 if let Some(p) = entry_peeled {
452 Some(p)
453 } else if let Some(cached_peel) =
454 packed.as_ref().and_then(|p| p.peeled(name, &oid))
455 {
456 cached_peel
457 } else if let Some(cached) = peel_cache.get(&oid) {
458 *cached
459 } else {
460 let p = odb.peel_tag_target(oid).ok().flatten();
461 peel_cache.insert(oid, p);
462 p
463 }
464 }
465 };
466 match f(AdvertisedRefView { name, oid, peeled }) {
467 Ok(ControlFlow::Continue(())) => ControlFlow::Continue(()),
468 Ok(ControlFlow::Break(())) => ControlFlow::Break(()),
469 Err(e) => {
470 *callback_err.borrow_mut() = Some(Err(e));
471 ControlFlow::Break(())
472 }
473 }
474 })?;
475 if callback_err.borrow().is_some() {
476 break;
477 }
478 }
479 if let Some(Err(e)) = callback_err.into_inner() {
480 return Err(e);
481 }
482 Ok(())
483 };
484 if peel_ctx {
485 odb.with_pack_read_context(run)
486 } else {
487 run()
488 }
489}
490
491pub(crate) fn advertised_object_ids_from_repo(
493 repo: &Repository,
494 hidden: &[String],
495 prefixes: &[&str],
496 peel: Peel,
497) -> Result<std::collections::HashSet<ObjectId>> {
498 let mut ids = std::collections::HashSet::new();
499 for_each_advertised_ref(repo, hidden, prefixes, peel, |r| {
500 ids.insert(r.oid);
501 if let Some(p) = r.peeled {
502 ids.insert(p);
503 }
504 Ok(ControlFlow::Continue(()))
505 })?;
506 Ok(ids)
507}
508
509fn scan_prefixes<'a>(requested: &[&'a str]) -> Vec<&'a str> {
517 const ALL: &str = "refs/";
518 if requested.is_empty() {
519 return vec![ALL];
520 }
521 let mut scan: Vec<&str> = requested
522 .iter()
523 .filter_map(|p| {
524 if p.starts_with(ALL) {
525 Some(*p)
526 } else {
527 ALL.starts_with(p).then_some(ALL)
528 }
529 })
530 .collect();
531 scan.sort_unstable();
532 scan.dedup_by(|later, kept| later.starts_with(*kept));
533 scan
534}
535
536pub(crate) struct HeadInfo {
538 pub(crate) target: Option<String>,
540 pub(crate) oid: Option<ObjectId>,
542}
543
544pub(crate) fn head_info(repo: &Repository) -> HeadInfo {
546 if repo.refs_on_disk() {
547 HeadInfo {
548 target: crate::refs::read_symbolic_ref(repo.git_dir(), "HEAD")
549 .ok()
550 .flatten(),
551 oid: crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok(),
552 }
553 } else {
554 HeadInfo {
555 target: crate::refs::read_symbolic_ref_for_repository(repo, "HEAD")
556 .ok()
557 .flatten(),
558 oid: crate::refs::resolve_ref_in(repo, "HEAD").ok(),
559 }
560 }
561}
562
563pub(crate) fn write_line(out: &mut dyn std::io::Write, data: &str) -> Result<()> {
565 let mut buf = Vec::with_capacity(data.len() + 5);
566 crate::pkt_line::write_line_to_vec(&mut buf, data)?;
567 out.write_all(&buf)?;
568 Ok(())
569}
570
571pub(crate) fn write_flush(out: &mut dyn std::io::Write) -> Result<()> {
573 out.write_all(crate::pkt_line::FLUSH.as_bytes())?;
574 Ok(())
575}
576
577pub(crate) fn read_packet(input: &mut dyn std::io::Read) -> Result<Option<Packet>> {
579 let pkt = crate::pkt_line::read_packet(&mut &mut *input)?;
580 Ok(pkt.map(|p| match p {
581 crate::pkt_line::Packet::Data(s) => Packet::Data(s.trim_end_matches('\n').to_owned()),
582 crate::pkt_line::Packet::Flush => Packet::Flush,
583 crate::pkt_line::Packet::Delim => Packet::Delim,
584 crate::pkt_line::Packet::ResponseEnd => Packet::ResponseEnd,
585 }))
586}
587
588#[derive(Debug, PartialEq, Eq)]
590pub(crate) enum Packet {
591 Data(String),
593 Flush,
595 Delim,
597 ResponseEnd,
599}
600
601pub(crate) fn parse_oid(repo: &Repository, hex: &str) -> Result<ObjectId> {
603 let oid = ObjectId::from_hex(hex)
604 .map_err(|_| ServeError::Protocol(format!("invalid object id '{hex}'")))?;
605 if oid.algo() != repo.odb().hash_algo() {
606 return Err(ServeError::Protocol(format!(
607 "object id '{hex}' does not match the repository hash algorithm"
608 )));
609 }
610 Ok(oid)
611}
612
613pub(crate) fn check_object_format(repo: &Repository, client: Option<&str>) -> Result<()> {
615 let ours = repo.odb().hash_algo().name();
616 match client {
617 Some(name) if name != ours => Err(ServeError::ObjectFormatMismatch {
618 client: name.to_owned(),
619 repository: ours,
620 }),
621 _ => Ok(()),
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::{
628 for_each_advertised_ref, scan_prefixes, AdvertisedRef, Peel, ProtocolVersion, Result,
629 };
630 use crate::config::environment::RepositoryOptions;
631 use crate::objects::{ObjectId, ObjectKind};
632 use crate::repo::{InitOptions, Repository};
633 use std::ops::ControlFlow;
634
635 #[test]
636 fn scan_prefixes_collapse_to_disjoint_sorted_ref_prefixes() {
637 assert_eq!(scan_prefixes(&[]), vec!["refs/"]);
638 assert_eq!(scan_prefixes(&["HEAD"]), Vec::<&str>::new());
639 assert_eq!(scan_prefixes(&["", "HEAD"]), vec!["refs/"]);
640 assert_eq!(scan_prefixes(&["re", "refs/tags/"]), vec!["refs/"]);
641 assert_eq!(
642 scan_prefixes(&[
643 "refs/tags/v1",
644 "refs/heads/",
645 "refs/tags/",
646 "refs/heads/main"
647 ]),
648 vec!["refs/heads/", "refs/tags/"]
649 );
650 assert_eq!(
651 scan_prefixes(&["refs/tags/v2", "refs/tags/v1", "refs/tags/v1"]),
652 vec!["refs/tags/v1", "refs/tags/v2"]
653 );
654 }
655
656 fn fixture() -> (tempfile::TempDir, Repository, ObjectId, ObjectId) {
659 let dir = tempfile::tempdir().expect("tempdir");
660 Repository::init(
661 dir.path(),
662 &InitOptions::new()
663 .with_bare(true)
664 .with_initial_branch("main"),
665 )
666 .expect("init");
667 let repo = Repository::open(dir.path(), &RepositoryOptions::empty()).expect("open");
668 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
669 let commit = repo
670 .odb()
671 .write(
672 ObjectKind::Commit,
673 format!("tree {tree}\nauthor A <a@e> 1 +0000\ncommitter A <a@e> 1 +0000\n\nc\n")
674 .as_bytes(),
675 )
676 .expect("commit");
677 let tag = repo
678 .odb()
679 .write(
680 ObjectKind::Tag,
681 format!("object {commit}\ntype commit\ntag v1\ntagger A <a@e> 1 +0000\n\nv1\n")
682 .as_bytes(),
683 )
684 .expect("tag");
685 let missing = ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").expect("oid");
686 for (name, oid) in [
687 ("refs/heads/main", commit),
688 ("refs/heads/feature", commit),
689 ("refs/heads/broken", missing),
690 ("refs/hidden/x", commit),
691 ("refs/tags/v1", tag),
692 ] {
693 let path = dir.path().join(name);
694 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
695 std::fs::write(path, format!("{oid}\n")).expect("ref");
696 }
697 (dir, repo, commit, tag)
698 }
699
700 fn advertised_refs(
701 repo: &Repository,
702 hidden: &[String],
703 prefixes: &[&str],
704 peel: Peel,
705 ) -> Result<Vec<AdvertisedRef>> {
706 let mut out = Vec::new();
707 for_each_advertised_ref(repo, hidden, prefixes, peel, |r| {
708 out.push(AdvertisedRef {
709 name: r.name.to_owned(),
710 oid: r.oid,
711 peeled: r.peeled,
712 });
713 Ok(ControlFlow::Continue(()))
714 })?;
715 Ok(out)
716 }
717
718 fn listed(refs: &[AdvertisedRef]) -> Vec<(&str, Option<ObjectId>)> {
719 refs.iter().map(|r| (r.name.as_str(), r.peeled)).collect()
720 }
721
722 #[cfg_attr(
723 not(feature = "full-tests"),
724 ignore = "starts system git or is heavy; run with --features full-tests"
725 )]
726 #[test]
727 fn advertised_refs_lists_requested_prefixes_and_skips_hidden() {
728 let (_dir, repo, commit, _tag) = fixture();
729 let hidden = vec!["refs/hidden".to_owned()];
730
731 let all = advertised_refs(&repo, &hidden, &[], Peel::Yes).expect("all");
732 assert_eq!(
733 listed(&all),
734 vec![
735 ("refs/heads/broken", None),
736 ("refs/heads/feature", None),
737 ("refs/heads/main", None),
738 ("refs/tags/v1", Some(commit)),
739 ]
740 );
741
742 let some = advertised_refs(
743 &repo,
744 &hidden,
745 &["refs/tags/", "refs/heads/m", "HEAD", "refs/hidden/"],
746 Peel::No,
747 )
748 .expect("prefixed");
749 assert_eq!(
750 listed(&some),
751 vec![("refs/heads/main", None), ("refs/tags/v1", None)]
752 );
753
754 let none = advertised_refs(&repo, &hidden, &["HEAD"], Peel::Yes).expect("HEAD only");
755 assert!(none.is_empty());
756 }
757
758 #[test]
759 fn git_protocol_picks_highest_version() {
760 assert_eq!(ProtocolVersion::from_git_protocol(""), ProtocolVersion::V0);
761 assert_eq!(
762 ProtocolVersion::from_git_protocol("version=1"),
763 ProtocolVersion::V1
764 );
765 assert_eq!(
766 ProtocolVersion::from_git_protocol("version=2:object-format=sha1"),
767 ProtocolVersion::V2
768 );
769 assert_eq!(
770 ProtocolVersion::from_git_protocol("version=9"),
771 ProtocolVersion::V0
772 );
773 }
774
775 #[test]
776 fn head_info_uses_injected_ref_store_for_symbolic_head() {
777 use std::sync::Arc;
778
779 use crate::refs::store::{
780 Expected, MemoryRefStore, RawRef, RefStore, RefTransaction, RefUpdate,
781 };
782 use crate::repo::RepositoryBuilder;
783
784 let dir = tempfile::tempdir().expect("tempdir");
785 Repository::init(
786 dir.path(),
787 &InitOptions::new()
788 .with_bare(true)
789 .with_initial_branch("master"),
790 )
791 .expect("init");
792 let store = Arc::new(MemoryRefStore::new());
793 let repo = RepositoryBuilder::new(RepositoryOptions::empty())
794 .ref_store(store.clone())
795 .initial_branch("master")
796 .open(dir.path(), None)
797 .expect("open");
798 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
799 let commit = repo
800 .odb()
801 .write(
802 ObjectKind::Commit,
803 format!(
804 "tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\ninit\n"
805 )
806 .as_bytes(),
807 )
808 .expect("commit");
809 store
810 .prepare(
811 RefTransaction::new()
812 .update(RefUpdate {
813 name: "refs/heads/master".into(),
814 new_value: Some(RawRef::Direct(commit)),
815 expected: Expected::Missing,
816 reflog: None,
817 flags: Default::default(),
818 })
819 .expect("txn"),
820 )
821 .expect("prepare")
822 .commit()
823 .expect("commit");
824
825 let head = super::head_info(&repo);
826 assert_eq!(head.target.as_deref(), Some("refs/heads/master"));
827 assert_eq!(head.oid, Some(commit));
828 }
829
830 #[test]
831 fn head_info_injected_unborn_head_has_symref_without_oid() {
832 use std::sync::Arc;
833
834 use crate::refs::store::MemoryRefStore;
835 use crate::repo::RepositoryBuilder;
836
837 let dir = tempfile::tempdir().expect("tempdir");
838 Repository::init(
839 dir.path(),
840 &InitOptions::new()
841 .with_bare(true)
842 .with_initial_branch("master"),
843 )
844 .expect("init");
845 let store = Arc::new(MemoryRefStore::new());
846 let repo = RepositoryBuilder::new(RepositoryOptions::empty())
847 .ref_store(store.clone())
848 .initial_branch("master")
849 .open(dir.path(), None)
850 .expect("open");
851
852 let head = super::head_info(&repo);
853 assert_eq!(head.target.as_deref(), Some("refs/heads/master"));
854 assert!(head.oid.is_none());
855 }
856}