mod receive_pack;
mod shallow_upload;
mod upload_pack;
mod upload_pack_policy;
pub(crate) use shallow_upload::{
compute_shallow_response, relative_deepen_depth_for_request, ShallowRequest,
};
pub use receive_pack::{
receive_pack, receive_pack_with_hook, HookRunRecord, ReceiveHook, ReceiveOutcome,
ReceivePolicy, RefCommand, RefUpdateRefusal, RefUpdateResult, UnpackError,
};
pub use upload_pack::upload_pack;
use std::ops::ControlFlow;
use crate::objects::ObjectId;
use crate::refs::listing::PACKED_PEELS_MIN_REFS;
use crate::refs::store::RawRef;
use crate::refs::PackedRefPeels;
use crate::repo::Repository;
#[derive(Debug, thiserror::Error)]
pub enum ServeError {
#[error("protocol error: {0}")]
Protocol(String),
#[error("not our ref: {0}")]
NotOurRef(ObjectId),
#[error("unknown command: {0}")]
UnknownCommand(String),
#[error("invalid deepen-not '{spec}': {reason}")]
InvalidDeepenNot {
spec: String,
reason: DeepenNotError,
},
#[error(transparent)]
UploadFilter(#[from] crate::upload_filter::UploadFilterError),
#[error("object format mismatch: client wants {client}, repository uses {repository}")]
ObjectFormatMismatch {
client: String,
repository: &'static str,
},
#[error(transparent)]
Repository(#[from] crate::error::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum DeepenNotError {
#[error("no such ref or object")]
UnknownRef,
#[error("matches more than one ref")]
AmbiguousRef,
#[error("does not point at a commit")]
NotACommit,
}
pub type Result<T> = std::result::Result<T, ServeError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProtocolVersion {
#[default]
V0,
V1,
V2,
}
impl ProtocolVersion {
#[must_use]
pub fn from_git_protocol(value: &str) -> Self {
value
.split(':')
.filter_map(|entry| entry.strip_prefix("version="))
.filter_map(|v| match v {
"2" => Some(Self::V2),
"1" => Some(Self::V1),
"0" => Some(Self::V0),
_ => None,
})
.max_by_key(|v| match v {
Self::V0 => 0,
Self::V1 => 1,
Self::V2 => 2,
})
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Default)]
pub struct ServeOptions {
pub protocol: ProtocolVersion,
pub stateless_rpc: bool,
pub advertise_refs: bool,
pub agent: String,
pub hidden_refs: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct AdvertisedRef {
pub(crate) name: String,
pub(crate) oid: ObjectId,
pub(crate) peeled: Option<ObjectId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Peel {
No,
Yes,
}
pub(crate) fn advertised_refs(
repo: &Repository,
hidden: &[String],
prefixes: &[&str],
peel: Peel,
) -> Result<Vec<AdvertisedRef>> {
let store = repo.refs();
let git_dir = repo.git_dir();
let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
let mut listed: Vec<(String, ObjectId)> = Vec::new();
for prefix in scan_prefixes(prefixes) {
store.for_each_ref(prefix, &mut |entry| {
let name = entry.name.as_str();
if !name.starts_with(prefix)
|| !crate::refs::is_valid_fetch_advertised_ref(name)
|| crate::hide_refs::ref_is_hidden(name, name, hidden)
|| (linked_worktree
&& !crate::worktree_ref::ref_visible_from_worktree(git_dir, name))
{
return ControlFlow::Continue(());
}
let oid = match &entry.value {
RawRef::Direct(oid) => Some(*oid),
RawRef::Symbolic(_) => entry.peeled.or_else(|| store.resolve(name).ok()),
};
if let Some(oid) = oid {
listed.push((entry.name.clone(), oid));
}
ControlFlow::Continue(())
})?;
}
if !listed.is_sorted_by(|a, b| a.0 < b.0) {
listed.sort_by(|a, b| a.0.cmp(&b.0));
listed.dedup_by(|later, first| later.0 == first.0);
}
if peel == Peel::No {
return Ok(listed
.into_iter()
.map(|(name, oid)| AdvertisedRef {
name,
oid,
peeled: None,
})
.collect());
}
let odb = repo.odb();
let packed = (listed.len() >= PACKED_PEELS_MIN_REFS)
.then(|| PackedRefPeels::for_store(store, git_dir).ok())
.flatten();
Ok(odb.with_pack_read_context(|| {
listed
.into_iter()
.map(|(name, oid)| {
let peeled = packed
.as_ref()
.and_then(|p| p.peeled(&name, &oid))
.unwrap_or_else(|| odb.peel_tag_target(oid).ok().flatten());
AdvertisedRef { name, oid, peeled }
})
.collect()
}))
}
fn scan_prefixes<'a>(requested: &[&'a str]) -> Vec<&'a str> {
const ALL: &str = "refs/";
if requested.is_empty() {
return vec![ALL];
}
let mut scan: Vec<&str> = requested
.iter()
.filter_map(|p| {
if p.starts_with(ALL) {
Some(*p)
} else {
ALL.starts_with(p).then_some(ALL)
}
})
.collect();
scan.sort_unstable();
scan.dedup_by(|later, kept| later.starts_with(*kept));
scan
}
pub(crate) struct HeadInfo {
pub(crate) target: Option<String>,
pub(crate) oid: Option<ObjectId>,
}
pub(crate) fn head_info(repo: &Repository) -> HeadInfo {
HeadInfo {
target: crate::refs::read_symbolic_ref(repo.git_dir(), "HEAD")
.ok()
.flatten(),
oid: crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok(),
}
}
pub(crate) fn write_line(out: &mut dyn std::io::Write, data: &str) -> Result<()> {
let mut buf = Vec::with_capacity(data.len() + 5);
crate::pkt_line::write_line_to_vec(&mut buf, data)?;
out.write_all(&buf)?;
Ok(())
}
pub(crate) fn write_flush(out: &mut dyn std::io::Write) -> Result<()> {
out.write_all(crate::pkt_line::FLUSH.as_bytes())?;
Ok(())
}
pub(crate) fn read_packet(input: &mut dyn std::io::Read) -> Result<Option<Packet>> {
let pkt = crate::pkt_line::read_packet(&mut &mut *input)?;
Ok(pkt.map(|p| match p {
crate::pkt_line::Packet::Data(s) => Packet::Data(s.trim_end_matches('\n').to_owned()),
crate::pkt_line::Packet::Flush => Packet::Flush,
crate::pkt_line::Packet::Delim => Packet::Delim,
crate::pkt_line::Packet::ResponseEnd => Packet::ResponseEnd,
}))
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Packet {
Data(String),
Flush,
Delim,
ResponseEnd,
}
pub(crate) fn parse_oid(repo: &Repository, hex: &str) -> Result<ObjectId> {
let oid = ObjectId::from_hex(hex)
.map_err(|_| ServeError::Protocol(format!("invalid object id '{hex}'")))?;
if oid.algo() != repo.odb().hash_algo() {
return Err(ServeError::Protocol(format!(
"object id '{hex}' does not match the repository hash algorithm"
)));
}
Ok(oid)
}
pub(crate) fn check_object_format(repo: &Repository, client: Option<&str>) -> Result<()> {
let ours = repo.odb().hash_algo().name();
match client {
Some(name) if name != ours => Err(ServeError::ObjectFormatMismatch {
client: name.to_owned(),
repository: ours,
}),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::{advertised_refs, scan_prefixes, Peel, ProtocolVersion};
use crate::config::environment::RepositoryOptions;
use crate::objects::{ObjectId, ObjectKind};
use crate::repo::{InitOptions, Repository};
#[test]
fn scan_prefixes_collapse_to_disjoint_sorted_ref_prefixes() {
assert_eq!(scan_prefixes(&[]), vec!["refs/"]);
assert_eq!(scan_prefixes(&["HEAD"]), Vec::<&str>::new());
assert_eq!(scan_prefixes(&["", "HEAD"]), vec!["refs/"]);
assert_eq!(scan_prefixes(&["re", "refs/tags/"]), vec!["refs/"]);
assert_eq!(
scan_prefixes(&[
"refs/tags/v1",
"refs/heads/",
"refs/tags/",
"refs/heads/main"
]),
vec!["refs/heads/", "refs/tags/"]
);
assert_eq!(
scan_prefixes(&["refs/tags/v2", "refs/tags/v1", "refs/tags/v1"]),
vec!["refs/tags/v1", "refs/tags/v2"]
);
}
fn fixture() -> (tempfile::TempDir, Repository, ObjectId, ObjectId) {
let dir = tempfile::tempdir().expect("tempdir");
Repository::init(
dir.path(),
&InitOptions::new()
.with_bare(true)
.with_initial_branch("main"),
)
.expect("init");
let repo = Repository::open(dir.path(), &RepositoryOptions::empty()).expect("open");
let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
let commit = repo
.odb()
.write(
ObjectKind::Commit,
format!("tree {tree}\nauthor A <a@e> 1 +0000\ncommitter A <a@e> 1 +0000\n\nc\n")
.as_bytes(),
)
.expect("commit");
let tag = repo
.odb()
.write(
ObjectKind::Tag,
format!("object {commit}\ntype commit\ntag v1\ntagger A <a@e> 1 +0000\n\nv1\n")
.as_bytes(),
)
.expect("tag");
let missing = ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").expect("oid");
for (name, oid) in [
("refs/heads/main", commit),
("refs/heads/feature", commit),
("refs/heads/broken", missing),
("refs/hidden/x", commit),
("refs/tags/v1", tag),
] {
let path = dir.path().join(name);
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(path, format!("{oid}\n")).expect("ref");
}
(dir, repo, commit, tag)
}
fn listed(refs: &[super::AdvertisedRef]) -> Vec<(&str, Option<ObjectId>)> {
refs.iter().map(|r| (r.name.as_str(), r.peeled)).collect()
}
#[test]
fn advertised_refs_lists_requested_prefixes_and_skips_hidden() {
let (_dir, repo, commit, _tag) = fixture();
let hidden = vec!["refs/hidden".to_owned()];
let all = advertised_refs(&repo, &hidden, &[], Peel::Yes).expect("all");
assert_eq!(
listed(&all),
vec![
("refs/heads/broken", None),
("refs/heads/feature", None),
("refs/heads/main", None),
("refs/tags/v1", Some(commit)),
]
);
let some = advertised_refs(
&repo,
&hidden,
&["refs/tags/", "refs/heads/m", "HEAD", "refs/hidden/"],
Peel::No,
)
.expect("prefixed");
assert_eq!(
listed(&some),
vec![("refs/heads/main", None), ("refs/tags/v1", None)]
);
let none = advertised_refs(&repo, &hidden, &["HEAD"], Peel::Yes).expect("HEAD only");
assert!(none.is_empty());
}
#[test]
fn git_protocol_picks_highest_version() {
assert_eq!(ProtocolVersion::from_git_protocol(""), ProtocolVersion::V0);
assert_eq!(
ProtocolVersion::from_git_protocol("version=1"),
ProtocolVersion::V1
);
assert_eq!(
ProtocolVersion::from_git_protocol("version=2:object-format=sha1"),
ProtocolVersion::V2
);
assert_eq!(
ProtocolVersion::from_git_protocol("version=9"),
ProtocolVersion::V0
);
}
}