1mod object_info;
21mod receive_pack;
22mod ref_update_policy;
23mod shallow_upload;
24mod upload_pack;
25mod upload_pack_policy;
26
27pub(crate) use shallow_upload::{compute_shallow_response, ShallowRequest};
28
29pub use receive_pack::{
30 receive_pack, receive_pack_with_hook, HookRunRecord, ReceiveHook, ReceiveOutcome,
31 ReceivePolicy, RefCommand, RefUpdateRefusal, RefUpdateResult, RejectedObject, UnpackError,
32};
33pub use ref_update_policy::{checked_out_branch, head_branch, validate_ref_update, HeadBranches};
34pub use upload_pack::upload_pack;
35
36use std::ops::ControlFlow;
37
38use crate::objects::ObjectId;
39use crate::refs::listing::PACKED_PEELS_MIN_REFS;
40use crate::refs::store::RawRef;
41use crate::refs::PackedRefPeels;
42use crate::repo::Repository;
43
44#[derive(Debug, thiserror::Error)]
46pub enum ServeError {
47 #[error("protocol error: {0}")]
49 Protocol(String),
50 #[error("not our ref: {0}")]
52 NotOurRef(ObjectId),
53 #[error("unknown command: {0}")]
55 UnknownCommand(String),
56 #[error("invalid deepen-not '{spec}': {reason}")]
59 InvalidDeepenNot {
60 spec: String,
62 reason: DeepenNotError,
64 },
65 #[error(transparent)]
67 UploadFilter(#[from] crate::upload_filter::UploadFilterError),
68 #[error("object format mismatch: client wants {client}, repository uses {repository}")]
70 ObjectFormatMismatch {
71 client: String,
73 repository: &'static str,
75 },
76 #[error(transparent)]
78 Repository(#[from] crate::error::Error),
79 #[error(transparent)]
81 Io(#[from] std::io::Error),
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
86pub enum DeepenNotError {
87 #[error("no such ref or object")]
89 UnknownRef,
90 #[error("matches more than one ref")]
92 AmbiguousRef,
93 #[error("does not point at a commit")]
95 NotACommit,
96}
97
98pub type Result<T> = std::result::Result<T, ServeError>;
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum ProtocolVersion {
108 #[default]
110 V0,
111 V1,
113 V2,
115}
116
117impl ProtocolVersion {
118 #[must_use]
132 pub fn from_git_protocol(value: &str) -> Self {
133 value
134 .split(':')
135 .filter_map(|entry| entry.strip_prefix("version="))
136 .filter_map(|v| match v {
137 "2" => Some(Self::V2),
138 "1" => Some(Self::V1),
139 "0" => Some(Self::V0),
140 _ => None,
141 })
142 .max_by_key(|v| match v {
143 Self::V0 => 0,
144 Self::V1 => 1,
145 Self::V2 => 2,
146 })
147 .unwrap_or_default()
148 }
149}
150
151#[derive(Debug, Clone, Default)]
153pub struct ServeOptions {
154 pub protocol: ProtocolVersion,
156 pub stateless_rpc: bool,
159 pub advertise_refs: bool,
162 pub agent: String,
164 pub hidden_refs: Vec<String>,
167 pub server_options: Vec<String>,
171 pub bundle_list: Option<crate::bundle_uri::BundleList>,
174 pub push_bundle: Option<String>,
178}
179
180pub const PUSH_BUNDLE_CAPABILITY: &str = "pushBundle";
183
184#[derive(Debug, Clone)]
186pub(crate) struct AdvertisedRef {
187 pub(crate) name: String,
189 pub(crate) oid: ObjectId,
191 pub(crate) peeled: Option<ObjectId>,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub(crate) enum Peel {
198 No,
200 Yes,
202}
203
204pub(crate) fn advertised_refs(
217 repo: &Repository,
218 hidden: &[String],
219 prefixes: &[&str],
220 peel: Peel,
221) -> Result<Vec<AdvertisedRef>> {
222 let store = repo.refs();
223 let git_dir = repo.git_dir();
224 let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
225 let mut listed: Vec<(String, ObjectId)> = Vec::new();
226 for prefix in scan_prefixes(prefixes) {
227 store.for_each_ref(prefix, &mut |entry| {
228 let name = entry.name.as_str();
229 if !name.starts_with(prefix)
230 || !crate::refs::is_valid_fetch_advertised_ref(name)
231 || crate::hide_refs::ref_is_hidden(name, name, hidden)
232 || (linked_worktree
233 && !crate::worktree_ref::ref_visible_from_worktree(git_dir, name))
234 {
235 return ControlFlow::Continue(());
236 }
237 let oid = match &entry.value {
238 RawRef::Direct(oid) => Some(*oid),
239 RawRef::Symbolic(_) => entry.peeled.or_else(|| store.resolve(name).ok()),
240 };
241 if let Some(oid) = oid {
242 listed.push((entry.name.clone(), oid));
243 }
244 ControlFlow::Continue(())
245 })?;
246 }
247 if !listed.is_sorted_by(|a, b| a.0 < b.0) {
248 listed.sort_by(|a, b| a.0.cmp(&b.0));
249 listed.dedup_by(|later, first| later.0 == first.0);
250 }
251
252 if peel == Peel::No {
253 return Ok(listed
254 .into_iter()
255 .map(|(name, oid)| AdvertisedRef {
256 name,
257 oid,
258 peeled: None,
259 })
260 .collect());
261 }
262 let odb = repo.odb();
263 let packed = (listed.len() >= PACKED_PEELS_MIN_REFS)
265 .then(|| PackedRefPeels::for_store(store, git_dir).ok())
266 .flatten();
267 Ok(odb.with_pack_read_context(|| {
268 listed
269 .into_iter()
270 .map(|(name, oid)| {
271 let peeled = packed
272 .as_ref()
273 .and_then(|p| p.peeled(&name, &oid))
274 .unwrap_or_else(|| odb.peel_tag_target(oid).ok().flatten());
275 AdvertisedRef { name, oid, peeled }
276 })
277 .collect()
278 }))
279}
280
281fn scan_prefixes<'a>(requested: &[&'a str]) -> Vec<&'a str> {
289 const ALL: &str = "refs/";
290 if requested.is_empty() {
291 return vec![ALL];
292 }
293 let mut scan: Vec<&str> = requested
294 .iter()
295 .filter_map(|p| {
296 if p.starts_with(ALL) {
297 Some(*p)
298 } else {
299 ALL.starts_with(p).then_some(ALL)
300 }
301 })
302 .collect();
303 scan.sort_unstable();
304 scan.dedup_by(|later, kept| later.starts_with(*kept));
305 scan
306}
307
308pub(crate) struct HeadInfo {
310 pub(crate) target: Option<String>,
312 pub(crate) oid: Option<ObjectId>,
314}
315
316pub(crate) fn head_info(repo: &Repository) -> HeadInfo {
318 HeadInfo {
319 target: crate::refs::read_symbolic_ref(repo.git_dir(), "HEAD")
320 .ok()
321 .flatten(),
322 oid: crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok(),
323 }
324}
325
326pub(crate) fn write_line(out: &mut dyn std::io::Write, data: &str) -> Result<()> {
328 let mut buf = Vec::with_capacity(data.len() + 5);
329 crate::pkt_line::write_line_to_vec(&mut buf, data)?;
330 out.write_all(&buf)?;
331 Ok(())
332}
333
334pub(crate) fn write_flush(out: &mut dyn std::io::Write) -> Result<()> {
336 out.write_all(crate::pkt_line::FLUSH.as_bytes())?;
337 Ok(())
338}
339
340pub(crate) fn read_packet(input: &mut dyn std::io::Read) -> Result<Option<Packet>> {
342 let pkt = crate::pkt_line::read_packet(&mut &mut *input)?;
343 Ok(pkt.map(|p| match p {
344 crate::pkt_line::Packet::Data(s) => Packet::Data(s.trim_end_matches('\n').to_owned()),
345 crate::pkt_line::Packet::Flush => Packet::Flush,
346 crate::pkt_line::Packet::Delim => Packet::Delim,
347 crate::pkt_line::Packet::ResponseEnd => Packet::ResponseEnd,
348 }))
349}
350
351#[derive(Debug, PartialEq, Eq)]
353pub(crate) enum Packet {
354 Data(String),
356 Flush,
358 Delim,
360 ResponseEnd,
362}
363
364pub(crate) fn parse_oid(repo: &Repository, hex: &str) -> Result<ObjectId> {
366 let oid = ObjectId::from_hex(hex)
367 .map_err(|_| ServeError::Protocol(format!("invalid object id '{hex}'")))?;
368 if oid.algo() != repo.odb().hash_algo() {
369 return Err(ServeError::Protocol(format!(
370 "object id '{hex}' does not match the repository hash algorithm"
371 )));
372 }
373 Ok(oid)
374}
375
376pub(crate) fn check_object_format(repo: &Repository, client: Option<&str>) -> Result<()> {
378 let ours = repo.odb().hash_algo().name();
379 match client {
380 Some(name) if name != ours => Err(ServeError::ObjectFormatMismatch {
381 client: name.to_owned(),
382 repository: ours,
383 }),
384 _ => Ok(()),
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::{advertised_refs, scan_prefixes, Peel, ProtocolVersion};
391 use crate::config::environment::RepositoryOptions;
392 use crate::objects::{ObjectId, ObjectKind};
393 use crate::repo::{InitOptions, Repository};
394
395 #[test]
396 fn scan_prefixes_collapse_to_disjoint_sorted_ref_prefixes() {
397 assert_eq!(scan_prefixes(&[]), vec!["refs/"]);
398 assert_eq!(scan_prefixes(&["HEAD"]), Vec::<&str>::new());
399 assert_eq!(scan_prefixes(&["", "HEAD"]), vec!["refs/"]);
400 assert_eq!(scan_prefixes(&["re", "refs/tags/"]), vec!["refs/"]);
401 assert_eq!(
402 scan_prefixes(&[
403 "refs/tags/v1",
404 "refs/heads/",
405 "refs/tags/",
406 "refs/heads/main"
407 ]),
408 vec!["refs/heads/", "refs/tags/"]
409 );
410 assert_eq!(
411 scan_prefixes(&["refs/tags/v2", "refs/tags/v1", "refs/tags/v1"]),
412 vec!["refs/tags/v1", "refs/tags/v2"]
413 );
414 }
415
416 fn fixture() -> (tempfile::TempDir, Repository, ObjectId, ObjectId) {
419 let dir = tempfile::tempdir().expect("tempdir");
420 Repository::init(
421 dir.path(),
422 &InitOptions::new()
423 .with_bare(true)
424 .with_initial_branch("main"),
425 )
426 .expect("init");
427 let repo = Repository::open(dir.path(), &RepositoryOptions::empty()).expect("open");
428 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
429 let commit = repo
430 .odb()
431 .write(
432 ObjectKind::Commit,
433 format!("tree {tree}\nauthor A <a@e> 1 +0000\ncommitter A <a@e> 1 +0000\n\nc\n")
434 .as_bytes(),
435 )
436 .expect("commit");
437 let tag = repo
438 .odb()
439 .write(
440 ObjectKind::Tag,
441 format!("object {commit}\ntype commit\ntag v1\ntagger A <a@e> 1 +0000\n\nv1\n")
442 .as_bytes(),
443 )
444 .expect("tag");
445 let missing = ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").expect("oid");
446 for (name, oid) in [
447 ("refs/heads/main", commit),
448 ("refs/heads/feature", commit),
449 ("refs/heads/broken", missing),
450 ("refs/hidden/x", commit),
451 ("refs/tags/v1", tag),
452 ] {
453 let path = dir.path().join(name);
454 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
455 std::fs::write(path, format!("{oid}\n")).expect("ref");
456 }
457 (dir, repo, commit, tag)
458 }
459
460 fn listed(refs: &[super::AdvertisedRef]) -> Vec<(&str, Option<ObjectId>)> {
461 refs.iter().map(|r| (r.name.as_str(), r.peeled)).collect()
462 }
463
464 #[cfg_attr(
465 not(feature = "full-tests"),
466 ignore = "starts system git or is heavy; run with --features full-tests"
467 )]
468 #[test]
469 fn advertised_refs_lists_requested_prefixes_and_skips_hidden() {
470 let (_dir, repo, commit, _tag) = fixture();
471 let hidden = vec!["refs/hidden".to_owned()];
472
473 let all = advertised_refs(&repo, &hidden, &[], Peel::Yes).expect("all");
474 assert_eq!(
475 listed(&all),
476 vec![
477 ("refs/heads/broken", None),
478 ("refs/heads/feature", None),
479 ("refs/heads/main", None),
480 ("refs/tags/v1", Some(commit)),
481 ]
482 );
483
484 let some = advertised_refs(
485 &repo,
486 &hidden,
487 &["refs/tags/", "refs/heads/m", "HEAD", "refs/hidden/"],
488 Peel::No,
489 )
490 .expect("prefixed");
491 assert_eq!(
492 listed(&some),
493 vec![("refs/heads/main", None), ("refs/tags/v1", None)]
494 );
495
496 let none = advertised_refs(&repo, &hidden, &["HEAD"], Peel::Yes).expect("HEAD only");
497 assert!(none.is_empty());
498 }
499
500 #[test]
501 fn git_protocol_picks_highest_version() {
502 assert_eq!(ProtocolVersion::from_git_protocol(""), ProtocolVersion::V0);
503 assert_eq!(
504 ProtocolVersion::from_git_protocol("version=1"),
505 ProtocolVersion::V1
506 );
507 assert_eq!(
508 ProtocolVersion::from_git_protocol("version=2:object-format=sha1"),
509 ProtocolVersion::V2
510 );
511 assert_eq!(
512 ProtocolVersion::from_git_protocol("version=9"),
513 ProtocolVersion::V0
514 );
515 }
516}