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, HookRunRecord, ReceiveOutcome, ReceivePolicy, RefUpdateResult,
};
pub use upload_pack::upload_pack;
use crate::objects::ObjectId;
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(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),
}
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>,
}
pub(crate) fn advertised_refs(repo: &Repository, hidden: &[String]) -> Result<Vec<AdvertisedRef>> {
let mut refs = crate::refs::list_refs_for_repository(repo, "refs/")?;
refs.sort_by(|a, b| a.0.cmp(&b.0));
let mut out = Vec::with_capacity(refs.len());
for (name, oid) in refs {
if !crate::refs::is_valid_fetch_advertised_ref(&name)
|| crate::hide_refs::ref_is_hidden(&name, &name, hidden)
{
continue;
}
let Ok(peeled) = repo.odb().peel_tags(oid).map(|(p, _)| p) else {
continue;
};
out.push(AdvertisedRef {
name,
oid,
peeled: (peeled != oid).then_some(peeled),
});
}
Ok(out)
}
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::ProtocolVersion;
#[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
);
}
}