1mod object_info;
21mod receive_pack;
22mod shallow_upload;
23mod upload_pack;
24mod upload_pack_policy;
25
26pub(crate) use shallow_upload::{compute_shallow_response, ShallowRequest};
27
28pub use receive_pack::{
29 receive_pack, receive_pack_with_hook, HookRunRecord, ReceiveHook, ReceiveOutcome,
30 ReceivePolicy, RefCommand, RefUpdateRefusal, RefUpdateResult, UnpackError,
31};
32pub use upload_pack::upload_pack;
33
34use std::ops::ControlFlow;
35
36use crate::objects::ObjectId;
37use crate::refs::listing::PACKED_PEELS_MIN_REFS;
38use crate::refs::store::RawRef;
39use crate::refs::PackedRefPeels;
40use crate::repo::Repository;
41
42#[derive(Debug, thiserror::Error)]
44pub enum ServeError {
45 #[error("protocol error: {0}")]
47 Protocol(String),
48 #[error("not our ref: {0}")]
50 NotOurRef(ObjectId),
51 #[error("unknown command: {0}")]
53 UnknownCommand(String),
54 #[error("invalid deepen-not '{spec}': {reason}")]
57 InvalidDeepenNot {
58 spec: String,
60 reason: DeepenNotError,
62 },
63 #[error(transparent)]
65 UploadFilter(#[from] crate::upload_filter::UploadFilterError),
66 #[error("object format mismatch: client wants {client}, repository uses {repository}")]
68 ObjectFormatMismatch {
69 client: String,
71 repository: &'static str,
73 },
74 #[error(transparent)]
76 Repository(#[from] crate::error::Error),
77 #[error(transparent)]
79 Io(#[from] std::io::Error),
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
84pub enum DeepenNotError {
85 #[error("no such ref or object")]
87 UnknownRef,
88 #[error("matches more than one ref")]
90 AmbiguousRef,
91 #[error("does not point at a commit")]
93 NotACommit,
94}
95
96pub type Result<T> = std::result::Result<T, ServeError>;
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum ProtocolVersion {
106 #[default]
108 V0,
109 V1,
111 V2,
113}
114
115impl ProtocolVersion {
116 #[must_use]
130 pub fn from_git_protocol(value: &str) -> Self {
131 value
132 .split(':')
133 .filter_map(|entry| entry.strip_prefix("version="))
134 .filter_map(|v| match v {
135 "2" => Some(Self::V2),
136 "1" => Some(Self::V1),
137 "0" => Some(Self::V0),
138 _ => None,
139 })
140 .max_by_key(|v| match v {
141 Self::V0 => 0,
142 Self::V1 => 1,
143 Self::V2 => 2,
144 })
145 .unwrap_or_default()
146 }
147}
148
149#[derive(Debug, Clone, Default)]
151pub struct ServeOptions {
152 pub protocol: ProtocolVersion,
154 pub stateless_rpc: bool,
157 pub advertise_refs: bool,
160 pub agent: String,
162 pub hidden_refs: Vec<String>,
165 pub server_options: Vec<String>,
169}
170
171#[derive(Debug, Clone)]
173pub(crate) struct AdvertisedRef {
174 pub(crate) name: String,
176 pub(crate) oid: ObjectId,
178 pub(crate) peeled: Option<ObjectId>,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub(crate) enum Peel {
185 No,
187 Yes,
189}
190
191pub(crate) fn advertised_refs(
204 repo: &Repository,
205 hidden: &[String],
206 prefixes: &[&str],
207 peel: Peel,
208) -> Result<Vec<AdvertisedRef>> {
209 let store = repo.refs();
210 let git_dir = repo.git_dir();
211 let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
212 let mut listed: Vec<(String, ObjectId)> = Vec::new();
213 for prefix in scan_prefixes(prefixes) {
214 store.for_each_ref(prefix, &mut |entry| {
215 let name = entry.name.as_str();
216 if !name.starts_with(prefix)
217 || !crate::refs::is_valid_fetch_advertised_ref(name)
218 || crate::hide_refs::ref_is_hidden(name, name, hidden)
219 || (linked_worktree
220 && !crate::worktree_ref::ref_visible_from_worktree(git_dir, name))
221 {
222 return ControlFlow::Continue(());
223 }
224 let oid = match &entry.value {
225 RawRef::Direct(oid) => Some(*oid),
226 RawRef::Symbolic(_) => entry.peeled.or_else(|| store.resolve(name).ok()),
227 };
228 if let Some(oid) = oid {
229 listed.push((entry.name.clone(), oid));
230 }
231 ControlFlow::Continue(())
232 })?;
233 }
234 if !listed.is_sorted_by(|a, b| a.0 < b.0) {
235 listed.sort_by(|a, b| a.0.cmp(&b.0));
236 listed.dedup_by(|later, first| later.0 == first.0);
237 }
238
239 if peel == Peel::No {
240 return Ok(listed
241 .into_iter()
242 .map(|(name, oid)| AdvertisedRef {
243 name,
244 oid,
245 peeled: None,
246 })
247 .collect());
248 }
249 let odb = repo.odb();
250 let packed = (listed.len() >= PACKED_PEELS_MIN_REFS)
252 .then(|| PackedRefPeels::for_store(store, git_dir).ok())
253 .flatten();
254 Ok(odb.with_pack_read_context(|| {
255 listed
256 .into_iter()
257 .map(|(name, oid)| {
258 let peeled = packed
259 .as_ref()
260 .and_then(|p| p.peeled(&name, &oid))
261 .unwrap_or_else(|| odb.peel_tag_target(oid).ok().flatten());
262 AdvertisedRef { name, oid, peeled }
263 })
264 .collect()
265 }))
266}
267
268fn scan_prefixes<'a>(requested: &[&'a str]) -> Vec<&'a str> {
276 const ALL: &str = "refs/";
277 if requested.is_empty() {
278 return vec![ALL];
279 }
280 let mut scan: Vec<&str> = requested
281 .iter()
282 .filter_map(|p| {
283 if p.starts_with(ALL) {
284 Some(*p)
285 } else {
286 ALL.starts_with(p).then_some(ALL)
287 }
288 })
289 .collect();
290 scan.sort_unstable();
291 scan.dedup_by(|later, kept| later.starts_with(*kept));
292 scan
293}
294
295pub(crate) struct HeadInfo {
297 pub(crate) target: Option<String>,
299 pub(crate) oid: Option<ObjectId>,
301}
302
303pub(crate) fn head_info(repo: &Repository) -> HeadInfo {
305 HeadInfo {
306 target: crate::refs::read_symbolic_ref(repo.git_dir(), "HEAD")
307 .ok()
308 .flatten(),
309 oid: crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok(),
310 }
311}
312
313pub(crate) fn write_line(out: &mut dyn std::io::Write, data: &str) -> Result<()> {
315 let mut buf = Vec::with_capacity(data.len() + 5);
316 crate::pkt_line::write_line_to_vec(&mut buf, data)?;
317 out.write_all(&buf)?;
318 Ok(())
319}
320
321pub(crate) fn write_flush(out: &mut dyn std::io::Write) -> Result<()> {
323 out.write_all(crate::pkt_line::FLUSH.as_bytes())?;
324 Ok(())
325}
326
327pub(crate) fn read_packet(input: &mut dyn std::io::Read) -> Result<Option<Packet>> {
329 let pkt = crate::pkt_line::read_packet(&mut &mut *input)?;
330 Ok(pkt.map(|p| match p {
331 crate::pkt_line::Packet::Data(s) => Packet::Data(s.trim_end_matches('\n').to_owned()),
332 crate::pkt_line::Packet::Flush => Packet::Flush,
333 crate::pkt_line::Packet::Delim => Packet::Delim,
334 crate::pkt_line::Packet::ResponseEnd => Packet::ResponseEnd,
335 }))
336}
337
338#[derive(Debug, PartialEq, Eq)]
340pub(crate) enum Packet {
341 Data(String),
343 Flush,
345 Delim,
347 ResponseEnd,
349}
350
351pub(crate) fn parse_oid(repo: &Repository, hex: &str) -> Result<ObjectId> {
353 let oid = ObjectId::from_hex(hex)
354 .map_err(|_| ServeError::Protocol(format!("invalid object id '{hex}'")))?;
355 if oid.algo() != repo.odb().hash_algo() {
356 return Err(ServeError::Protocol(format!(
357 "object id '{hex}' does not match the repository hash algorithm"
358 )));
359 }
360 Ok(oid)
361}
362
363pub(crate) fn check_object_format(repo: &Repository, client: Option<&str>) -> Result<()> {
365 let ours = repo.odb().hash_algo().name();
366 match client {
367 Some(name) if name != ours => Err(ServeError::ObjectFormatMismatch {
368 client: name.to_owned(),
369 repository: ours,
370 }),
371 _ => Ok(()),
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::{advertised_refs, scan_prefixes, Peel, ProtocolVersion};
378 use crate::config::environment::RepositoryOptions;
379 use crate::objects::{ObjectId, ObjectKind};
380 use crate::repo::{InitOptions, Repository};
381
382 #[test]
383 fn scan_prefixes_collapse_to_disjoint_sorted_ref_prefixes() {
384 assert_eq!(scan_prefixes(&[]), vec!["refs/"]);
385 assert_eq!(scan_prefixes(&["HEAD"]), Vec::<&str>::new());
386 assert_eq!(scan_prefixes(&["", "HEAD"]), vec!["refs/"]);
387 assert_eq!(scan_prefixes(&["re", "refs/tags/"]), vec!["refs/"]);
388 assert_eq!(
389 scan_prefixes(&[
390 "refs/tags/v1",
391 "refs/heads/",
392 "refs/tags/",
393 "refs/heads/main"
394 ]),
395 vec!["refs/heads/", "refs/tags/"]
396 );
397 assert_eq!(
398 scan_prefixes(&["refs/tags/v2", "refs/tags/v1", "refs/tags/v1"]),
399 vec!["refs/tags/v1", "refs/tags/v2"]
400 );
401 }
402
403 fn fixture() -> (tempfile::TempDir, Repository, ObjectId, ObjectId) {
406 let dir = tempfile::tempdir().expect("tempdir");
407 Repository::init(
408 dir.path(),
409 &InitOptions::new()
410 .with_bare(true)
411 .with_initial_branch("main"),
412 )
413 .expect("init");
414 let repo = Repository::open(dir.path(), &RepositoryOptions::empty()).expect("open");
415 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
416 let commit = repo
417 .odb()
418 .write(
419 ObjectKind::Commit,
420 format!("tree {tree}\nauthor A <a@e> 1 +0000\ncommitter A <a@e> 1 +0000\n\nc\n")
421 .as_bytes(),
422 )
423 .expect("commit");
424 let tag = repo
425 .odb()
426 .write(
427 ObjectKind::Tag,
428 format!("object {commit}\ntype commit\ntag v1\ntagger A <a@e> 1 +0000\n\nv1\n")
429 .as_bytes(),
430 )
431 .expect("tag");
432 let missing = ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").expect("oid");
433 for (name, oid) in [
434 ("refs/heads/main", commit),
435 ("refs/heads/feature", commit),
436 ("refs/heads/broken", missing),
437 ("refs/hidden/x", commit),
438 ("refs/tags/v1", tag),
439 ] {
440 let path = dir.path().join(name);
441 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
442 std::fs::write(path, format!("{oid}\n")).expect("ref");
443 }
444 (dir, repo, commit, tag)
445 }
446
447 fn listed(refs: &[super::AdvertisedRef]) -> Vec<(&str, Option<ObjectId>)> {
448 refs.iter().map(|r| (r.name.as_str(), r.peeled)).collect()
449 }
450
451 #[test]
452 fn advertised_refs_lists_requested_prefixes_and_skips_hidden() {
453 let (_dir, repo, commit, _tag) = fixture();
454 let hidden = vec!["refs/hidden".to_owned()];
455
456 let all = advertised_refs(&repo, &hidden, &[], Peel::Yes).expect("all");
457 assert_eq!(
458 listed(&all),
459 vec![
460 ("refs/heads/broken", None),
461 ("refs/heads/feature", None),
462 ("refs/heads/main", None),
463 ("refs/tags/v1", Some(commit)),
464 ]
465 );
466
467 let some = advertised_refs(
468 &repo,
469 &hidden,
470 &["refs/tags/", "refs/heads/m", "HEAD", "refs/hidden/"],
471 Peel::No,
472 )
473 .expect("prefixed");
474 assert_eq!(
475 listed(&some),
476 vec![("refs/heads/main", None), ("refs/tags/v1", None)]
477 );
478
479 let none = advertised_refs(&repo, &hidden, &["HEAD"], Peel::Yes).expect("HEAD only");
480 assert!(none.is_empty());
481 }
482
483 #[test]
484 fn git_protocol_picks_highest_version() {
485 assert_eq!(ProtocolVersion::from_git_protocol(""), ProtocolVersion::V0);
486 assert_eq!(
487 ProtocolVersion::from_git_protocol("version=1"),
488 ProtocolVersion::V1
489 );
490 assert_eq!(
491 ProtocolVersion::from_git_protocol("version=2:object-format=sha1"),
492 ProtocolVersion::V2
493 );
494 assert_eq!(
495 ProtocolVersion::from_git_protocol("version=9"),
496 ProtocolVersion::V0
497 );
498 }
499}