grit_lib/serve/mod.rs
1//! Server side of the Git wire protocol: answering fetches and accepting pushes.
2//!
3//! This module lets a process act as the remote end of a clone, fetch, or push.
4//! [`upload_pack`] serves objects to a fetching client (protocol v0, v1, and v2)
5//! and [`receive_pack`] accepts a pushed pack plus ref updates. Both operate on
6//! plain byte streams, so the same code serves a duplex connection (ssh, a
7//! local pipe) and the stateless request/response exchanges of smart HTTP.
8//!
9//! A push server decides what to accept with a [`ReceivePolicy`] (the
10//! `receive.*` settings), the repository's hook scripts, and optionally an
11//! in-process [`ReceiveHook`] passed to [`receive_pack_with_hook`]. Refusals
12//! come back typed, as [`RefUpdateRefusal`] per ref and [`UnpackError`] for the
13//! pack.
14//!
15//! The functions take an already-opened [`Repository`] and explicit options; they
16//! never read the process environment or print to the terminal. Callers are
17//! expected to map `GIT_PROTOCOL` to a [`ProtocolVersion`] and to load
18//! configuration at their own boundary.
19
20mod connectivity;
21mod object_info;
22pub(crate) mod promised_objects;
23mod receive_pack;
24mod ref_update_policy;
25mod shallow_upload;
26mod upload_pack;
27mod upload_pack_policy;
28
29pub(crate) use receive_pack::ReceiveHookScripts;
30pub(crate) use shallow_upload::{compute_shallow_response, ShallowRequest};
31
32pub use receive_pack::{
33 receive_pack, receive_pack_with_hook, receive_pack_with_hook_cancellable, HookRunRecord,
34 ReceiveHook, ReceiveHookContext, ReceiveOutcome, ReceivePolicy, RefCommand, RefUpdateRefusal,
35 RefUpdateResult, RejectedObject, UnpackError,
36};
37pub use ref_update_policy::{checked_out_branch, head_branch, validate_ref_update, HeadBranches};
38pub use upload_pack::{upload_pack, upload_pack_cancellable, UploadOutcome};
39
40use std::cell::RefCell;
41use std::collections::HashMap;
42use std::ops::ControlFlow;
43
44use crate::objects::ObjectId;
45use crate::refs::store::RawRef;
46use crate::refs::PackedRefPeels;
47use crate::repo::Repository;
48
49/// Errors produced while serving a fetch or a push.
50#[derive(Debug, thiserror::Error)]
51pub enum ServeError {
52 /// The peer sent something that does not follow the wire protocol.
53 #[error("protocol error: {0}")]
54 Protocol(String),
55 /// The client asked for an object that is not an advertised ref tip.
56 #[error("not our ref: {0}")]
57 NotOurRef(ObjectId),
58 /// An object the response needs is not stored in the served repository.
59 ///
60 /// Only raised before any pack byte is written, so the client gets a clean
61 /// `ERR` line (protocol v2) or a side-band error (v0/v1) instead of a pack cut
62 /// short. Repositories without promisor remotes report missing objects the
63 /// way they always did, as a [`ServeError::Repository`] error.
64 #[error("object {oid} is missing: {reason}")]
65 ObjectMissing {
66 /// The missing object.
67 oid: ObjectId,
68 /// Why it could not be sent.
69 reason: MissingObjectReason,
70 },
71 /// The client requested a v2 command this server does not implement.
72 #[error("unknown command: {0}")]
73 UnknownCommand(String),
74 /// A `deepen-not` argument (`--shallow-exclude`) cannot be used as a
75 /// shallow-history exclusion point.
76 #[error("invalid deepen-not '{spec}': {reason}")]
77 InvalidDeepenNot {
78 /// The argument exactly as the client sent it.
79 spec: String,
80 /// Why it was rejected.
81 reason: DeepenNotError,
82 },
83 /// An upload-pack filter request was rejected by repository config.
84 #[error(transparent)]
85 UploadFilter(#[from] crate::upload_filter::UploadFilterError),
86 /// The client and repository disagree on the object hash algorithm.
87 #[error("object format mismatch: client wants {client}, repository uses {repository}")]
88 ObjectFormatMismatch {
89 /// Hash algorithm named by the client.
90 client: String,
91 /// Hash algorithm of the served repository.
92 repository: &'static str,
93 },
94 /// Reading or writing repository data failed.
95 #[error(transparent)]
96 Repository(#[from] crate::error::Error),
97 /// The underlying stream failed.
98 #[error(transparent)]
99 Io(#[from] std::io::Error),
100 /// The session was stopped by a cancel signal before finishing.
101 #[error("cancelled")]
102 Cancelled,
103 /// Sending the pack failed after part of it had gone out, so the response
104 /// cannot be turned into a clean error any more. With a side band the
105 /// client was told on band 3; the transport should end the response as
106 /// failed rather than complete it.
107 #[error("the pack failed partway through: {0}")]
108 PackInterrupted(Box<ServeError>),
109}
110
111/// A fetch request body grew past the transport's decoded-size limit.
112///
113/// Smart HTTP wraps upload-pack input with a byte cap. The wrapper surfaces this
114/// as the inner error of [`ServeError::Io`]. Upload-pack must return that error
115/// to the transport instead of encoding it as a protocol v2 `ERR` line.
116#[derive(Debug)]
117pub struct RequestBodyLimitExceeded;
118
119impl std::fmt::Display for RequestBodyLimitExceeded {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(f, "fetch request body exceeds the decoded size limit")
122 }
123}
124
125impl std::error::Error for RequestBodyLimitExceeded {}
126
127/// Whether `err` is a fetch request-body limit from a transport wrapper.
128#[must_use]
129pub fn io_error_is_request_body_limit(err: &std::io::Error) -> bool {
130 err.get_ref()
131 .is_some_and(|inner| inner.is::<RequestBodyLimitExceeded>())
132}
133
134/// Why a missing object could not be served; see [`ServeError::ObjectMissing`].
135///
136/// A repository whose large blobs live on a large-object promisor (LOP) leaves
137/// them out of its own packs. Such a blob is *promised*: some object in a promisor
138/// pack refers to it. A client that accepted one of the advertised promisor
139/// remotes is sent the pack without it and fetches it from the LOP itself; any
140/// other client needs the server to fetch it first (lazy fetch, see
141/// [`ServeOptions::lazy_fetch`]) or gets this error.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
143#[non_exhaustive]
144pub enum MissingObjectReason {
145 /// The object can be fetched from a promisor remote, but the client accepted
146 /// none of them and lazy fetch is off.
147 #[error("it is stored on a promisor remote the client did not accept")]
148 Promised,
149 /// No promisor object refers to it: the repository is missing an object it
150 /// should have.
151 #[error("no promisor remote has it either")]
152 NotPromised,
153 /// Lazy fetch was requested, but the [`Repository`] was opened without a
154 /// [`PromisorFetcher`](crate::promisor_fetch::PromisorFetcher).
155 #[error("lazy fetch is on but no promisor fetcher is installed")]
156 NoFetcher,
157 /// Lazy fetch ran and the object is still not stored.
158 #[error("fetching it from the promisor remotes did not bring it in")]
159 LazyFetchFailed,
160}
161
162/// Why a `deepen-not` argument was rejected; see [`ServeError::InvalidDeepenNot`].
163#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
164pub enum DeepenNotError {
165 /// The argument names neither an existing ref nor an object in the repository.
166 #[error("no such ref or object")]
167 UnknownRef,
168 /// A short name matched more than one ref under the ref lookup rules.
169 #[error("matches more than one ref")]
170 AmbiguousRef,
171 /// The ref or object id does not lead to a commit once tags are peeled.
172 #[error("does not point at a commit")]
173 NotACommit,
174}
175
176/// Result alias for serving operations.
177pub type Result<T> = std::result::Result<T, ServeError>;
178
179/// Wire protocol version requested by the client.
180///
181/// Version 1 is version 0 with an explicit `version 1` line in front of the
182/// advertisement. Version 2 replaces the ref advertisement with a capability
183/// list and command requests.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185pub enum ProtocolVersion {
186 /// The original protocol, used when the client asks for nothing else.
187 #[default]
188 V0,
189 /// Version 0 framing announced with a `version 1` line.
190 V1,
191 /// Command-based protocol version 2.
192 V2,
193}
194
195impl ProtocolVersion {
196 /// Pick the highest version named in a `GIT_PROTOCOL` style value.
197 ///
198 /// The value is a colon-separated list of `key=value` entries; only
199 /// `version=N` entries are considered. Unknown or missing versions fall
200 /// back to [`ProtocolVersion::V0`].
201 ///
202 /// # Parameters
203 ///
204 /// - `value`: the raw value, for example `version=2` or `version=2:foo=bar`.
205 ///
206 /// # Returns
207 ///
208 /// The highest supported version the value asks for.
209 #[must_use]
210 pub fn from_git_protocol(value: &str) -> Self {
211 value
212 .split(':')
213 .filter_map(|entry| entry.strip_prefix("version="))
214 .filter_map(|v| match v {
215 "2" => Some(Self::V2),
216 "1" => Some(Self::V1),
217 "0" => Some(Self::V0),
218 _ => None,
219 })
220 .max_by_key(|v| match v {
221 Self::V0 => 0,
222 Self::V1 => 1,
223 Self::V2 => 2,
224 })
225 .unwrap_or_default()
226 }
227}
228
229/// How a serving session is framed on the wire.
230#[derive(Debug, Clone, Default)]
231pub struct ServeOptions {
232 /// Protocol version the client asked for.
233 pub protocol: ProtocolVersion,
234 /// Handle exactly one request from the input and stop, without printing the
235 /// advertisement first. Smart HTTP uses this for its POST requests.
236 pub stateless_rpc: bool,
237 /// Print the ref (or capability) advertisement and stop. Smart HTTP uses
238 /// this to answer `GET info/refs`.
239 pub advertise_refs: bool,
240 /// Value for the `agent=` capability, such as `grit/0.5.0`.
241 pub agent: String,
242 /// Ref patterns hidden from the advertisement (`transfer.hideRefs` and the
243 /// service-specific `uploadpack.hideRefs` / `receive.hideRefs`).
244 pub hidden_refs: Vec<String>,
245 /// Values the client sent as v2 `server-option=<value>` capability lines on
246 /// the current command request. Cleared at the start of each v2 command and
247 /// filled before `ls-refs` / `fetch` / `object-info` handlers run.
248 pub server_options: Vec<String>,
249 /// Bundles to offer through the protocol v2 `bundle-uri` command. `None`
250 /// leaves the command out of the advertisement.
251 pub bundle_list: Option<crate::bundle_uri::BundleList>,
252 /// Where clients may upload a resumable push bundle, advertised by
253 /// receive-pack as `pushBundle=<value>`; relative values are relative to
254 /// the repository URL. `None` advertises nothing.
255 pub push_bundle: Option<String>,
256 /// Filled while serving: the promisor remotes the client accepted, from the
257 /// `promisor-remote=<names>` capability of its v2 command requests. Only names
258 /// this server advertised (see
259 /// [`PromisorAdvertisement`](crate::promisor_remote::PromisorAdvertisement))
260 /// are kept. The list lasts for the whole session, so an acceptance sent with
261 /// `ls-refs` still holds for the `fetch` that follows; a stateless (smart
262 /// HTTP) request only sees what that one request carried. Callers leave it
263 /// empty.
264 pub accepted_promisor_remotes: Vec<String>,
265 /// Fetch objects this repository is missing from its own promisor remotes
266 /// before answering a client that accepted none of them, so the client still
267 /// gets a complete pack. `None` uses `uploadpack.lazyFetch` (a grit setting,
268 /// false by default).
269 ///
270 /// Git's upload-pack turns lazy fetch off unless `GIT_NO_LAZY_FETCH=0` is in
271 /// its environment; the library reads no environment, so callers map that
272 /// variable here. Fetching needs a
273 /// [`PromisorFetcher`](crate::promisor_fetch::PromisorFetcher) on the
274 /// [`Repository`], otherwise the request fails with
275 /// [`MissingObjectReason::NoFetcher`].
276 pub lazy_fetch: Option<bool>,
277}
278
279impl ServeOptions {
280 /// The [`Self::lazy_fetch`] an upload-pack process should use under `env`, from
281 /// `GIT_NO_LAZY_FETCH` (or grit's `GRIT_NO_LAZY_FETCH`, which wins when both are
282 /// set): a false value turns lazy fetch on, a true value turns it off, and
283 /// unset (or not a boolean) leaves `uploadpack.lazyFetch` in charge.
284 ///
285 /// Git's upload-pack sets `GIT_NO_LAZY_FETCH=1` unless the variable is already
286 /// in its environment, so `GIT_NO_LAZY_FETCH=0` is how a Git server is told to
287 /// fetch promised objects for clients that did not accept its promisor remotes.
288 ///
289 /// # Parameters
290 ///
291 /// - `env`: the server process environment.
292 #[must_use]
293 pub fn lazy_fetch_from_environment(env: &crate::environment::Environment) -> Option<bool> {
294 [
295 crate::promisor_fetch::NO_LAZY_FETCH_ENV,
296 "GIT_NO_LAZY_FETCH",
297 ]
298 .into_iter()
299 .find_map(|key| env.var(key))
300 .and_then(|value| crate::config::parse_bool(value.trim()).ok())
301 .map(|no_lazy_fetch| !no_lazy_fetch)
302 }
303
304 /// Whether upload-pack may fetch missing promised objects for this session:
305 /// [`Self::lazy_fetch`] when set, otherwise `uploadpack.lazyFetch` from `config`.
306 ///
307 /// A caller uses this to decide whether to open the served [`Repository`] with a
308 /// [`PromisorFetcher`](crate::promisor_fetch::PromisorFetcher). Without one, a
309 /// served repository never fetches anything.
310 ///
311 /// # Parameters
312 ///
313 /// - `config`: the served repository's configuration.
314 #[must_use]
315 pub fn lazy_fetch_enabled(&self, config: &crate::config::ConfigSet) -> bool {
316 self.lazy_fetch.unwrap_or_else(|| {
317 config
318 .get_bool("uploadpack.lazyfetch")
319 .and_then(|v| v.ok())
320 .unwrap_or(false)
321 })
322 }
323
324 /// Give the repository an upload-pack session serves the promisor fetcher it may
325 /// use: `fetcher` when [`Self::lazy_fetch_enabled`], and none otherwise, so a
326 /// server without lazy fetch never fetches anything, whichever fetcher the
327 /// repository was opened with.
328 ///
329 /// Every upload-pack entry point (the `grit upload-pack` command, smart HTTP,
330 /// the in-process local transport) calls this right after opening the
331 /// repository.
332 ///
333 /// # Parameters
334 ///
335 /// - `repo`: the served repository.
336 /// - `config`: its configuration.
337 /// - `fetcher`: the fetcher to use when lazy fetch is on.
338 ///
339 /// # Returns
340 ///
341 /// Whether a fetcher is now installed.
342 pub fn attach_promisor_fetcher(
343 &self,
344 repo: &Repository,
345 config: &crate::config::ConfigSet,
346 fetcher: Option<std::sync::Arc<dyn crate::promisor_fetch::PromisorFetcher>>,
347 ) -> bool {
348 let fetcher = fetcher.filter(|_| self.lazy_fetch_enabled(config));
349 let attached = fetcher.is_some();
350 repo.caches().set_promisor_fetcher(fetcher);
351 attached
352 }
353}
354
355/// Name of the receive-pack capability that advertises a resumable push
356/// endpoint ([`ServeOptions::push_bundle`]).
357pub const PUSH_BUNDLE_CAPABILITY: &str = "pushBundle";
358
359/// One ref as it appears in an advertisement.
360#[derive(Debug, Clone)]
361#[cfg_attr(not(test), allow(dead_code))]
362pub(crate) struct AdvertisedRef {
363 /// Full ref name, such as `refs/heads/main`.
364 pub(crate) name: String,
365 /// Object the ref points at.
366 pub(crate) oid: ObjectId,
367 /// For an annotated tag, the non-tag object it ultimately points at.
368 pub(crate) peeled: Option<ObjectId>,
369}
370
371/// Whether [`advertised_refs`] fills [`AdvertisedRef::peeled`].
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub(crate) enum Peel {
374 /// Leave `peeled` empty and read no objects.
375 No,
376 /// Peel annotated tags, trusting `packed-refs` peel lines where they apply.
377 Yes,
378}
379
380/// One ref while streaming an advertisement (name borrows the ref store entry).
381#[derive(Debug, Clone, Copy)]
382pub(crate) struct AdvertisedRefView<'a> {
383 /// Full ref name, such as `refs/heads/main`.
384 pub(crate) name: &'a str,
385 /// Object the ref points at.
386 pub(crate) oid: ObjectId,
387 /// For an annotated tag, the non-tag object it ultimately points at.
388 pub(crate) peeled: Option<ObjectId>,
389}
390
391/// Visit refs in advertisement order without retaining every name.
392///
393/// Only refs whose names start with one of `prefixes` are listed (every ref
394/// under `refs/` when `prefixes` is empty), and the ref store is asked for
395/// those prefixes alone, so the cost follows the refs returned rather than the
396/// refs stored. Prefixes that cannot name a ref under `refs/` (such as `HEAD`)
397/// select nothing here.
398///
399/// Refs whose names are not safe to send and symbolic refs that do not resolve
400/// are left out. As in Git, a ref naming a missing object is still advertised;
401/// with [`Peel::Yes`] it, like any ref whose tag chain cannot be read, simply
402/// gets no peeled value.
403pub(crate) fn for_each_advertised_ref(
404 repo: &Repository,
405 hidden: &[String],
406 prefixes: &[&str],
407 peel: Peel,
408 mut f: impl FnMut(AdvertisedRefView<'_>) -> Result<ControlFlow<()>>,
409) -> Result<()> {
410 let store = repo.refs();
411 let git_dir = repo.git_dir();
412 let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
413 let packed = (peel == Peel::Yes)
414 .then(|| PackedRefPeels::for_store(store, git_dir).ok())
415 .flatten();
416 let odb = repo.odb();
417 let mut peel_cache: HashMap<ObjectId, Option<ObjectId>> = HashMap::new();
418 let callback_err = RefCell::new(None::<Result<()>>);
419 let peel_ctx = peel == Peel::Yes;
420 let run = || {
421 for prefix in scan_prefixes(prefixes) {
422 store.for_each_ref(prefix, &mut |entry| {
423 if callback_err.borrow().is_some() {
424 return ControlFlow::Break(());
425 }
426 let name = entry.name.as_str();
427 if !name.starts_with(prefix)
428 || !crate::refs::is_valid_fetch_advertised_ref(name)
429 || crate::hide_refs::ref_is_hidden(name, name, hidden)
430 || (linked_worktree
431 && !crate::worktree_ref::ref_visible_from_worktree(git_dir, name))
432 {
433 return ControlFlow::Continue(());
434 }
435 let oid = match &entry.value {
436 RawRef::Direct(oid) => Some(*oid),
437 RawRef::Symbolic(_) => entry.peeled.or_else(|| store.resolve(name).ok()),
438 };
439 let Some(oid) = oid else {
440 return ControlFlow::Continue(());
441 };
442 let entry_peeled = match &entry.value {
443 RawRef::Direct(_) => entry.peeled,
444 RawRef::Symbolic(_) => None,
445 };
446 let peeled: Option<ObjectId> = match peel {
447 Peel::No => None,
448 Peel::Yes => {
449 if let Some(p) = entry_peeled {
450 Some(p)
451 } else if let Some(cached_peel) =
452 packed.as_ref().and_then(|p| p.peeled(name, &oid))
453 {
454 cached_peel
455 } else if let Some(cached) = peel_cache.get(&oid) {
456 *cached
457 } else {
458 let p = odb.peel_tag_target(oid).ok().flatten();
459 peel_cache.insert(oid, p);
460 p
461 }
462 }
463 };
464 match f(AdvertisedRefView { name, oid, peeled }) {
465 Ok(ControlFlow::Continue(())) => ControlFlow::Continue(()),
466 Ok(ControlFlow::Break(())) => ControlFlow::Break(()),
467 Err(e) => {
468 *callback_err.borrow_mut() = Some(Err(e));
469 ControlFlow::Break(())
470 }
471 }
472 })?;
473 if callback_err.borrow().is_some() {
474 break;
475 }
476 }
477 if let Some(Err(e)) = callback_err.into_inner() {
478 return Err(e);
479 }
480 Ok(())
481 };
482 if peel_ctx {
483 odb.with_pack_read_context(run)
484 } else {
485 run()
486 }
487}
488
489/// Collect every advertised ref (tests and callers that need the full list).
490#[cfg_attr(not(test), allow(dead_code))]
491pub(crate) fn advertised_refs(
492 repo: &Repository,
493 hidden: &[String],
494 prefixes: &[&str],
495 peel: Peel,
496) -> Result<Vec<AdvertisedRef>> {
497 let mut out = Vec::new();
498 for_each_advertised_ref(repo, hidden, prefixes, peel, |r| {
499 out.push(AdvertisedRef {
500 name: r.name.to_owned(),
501 oid: r.oid,
502 peeled: r.peeled,
503 });
504 Ok(ControlFlow::Continue(()))
505 })?;
506 Ok(out)
507}
508
509/// Object ids that appear in an advertisement (direct tips and peeled targets).
510pub(crate) fn advertised_object_ids_from_repo(
511 repo: &Repository,
512 hidden: &[String],
513 prefixes: &[&str],
514 peel: Peel,
515) -> Result<std::collections::HashSet<ObjectId>> {
516 let mut ids = std::collections::HashSet::new();
517 for_each_advertised_ref(repo, hidden, prefixes, peel, |r| {
518 ids.insert(r.oid);
519 if let Some(p) = r.peeled {
520 ids.insert(p);
521 }
522 Ok(ControlFlow::Continue(()))
523 })?;
524 Ok(ids)
525}
526
527/// Ref-store prefixes to list so that every ref under `refs/` starting with one
528/// of `requested` is visited exactly once.
529///
530/// No request means all of `refs/`. A request shorter than `refs/` that it
531/// begins (`""`, `"re"`) widens to `refs/`; one outside it selects nothing.
532/// Prefixes covered by a shorter one are dropped, which leaves the result
533/// sorted and disjoint: listing each in turn yields names in sorted order.
534fn scan_prefixes<'a>(requested: &[&'a str]) -> Vec<&'a str> {
535 const ALL: &str = "refs/";
536 if requested.is_empty() {
537 return vec![ALL];
538 }
539 let mut scan: Vec<&str> = requested
540 .iter()
541 .filter_map(|p| {
542 if p.starts_with(ALL) {
543 Some(*p)
544 } else {
545 ALL.starts_with(p).then_some(ALL)
546 }
547 })
548 .collect();
549 scan.sort_unstable();
550 scan.dedup_by(|later, kept| later.starts_with(*kept));
551 scan
552}
553
554/// Where `HEAD` points: its symbolic target (if any) and resolved object (if born).
555pub(crate) struct HeadInfo {
556 /// Ref `HEAD` names, such as `refs/heads/main`, when `HEAD` is symbolic.
557 pub(crate) target: Option<String>,
558 /// Object `HEAD` resolves to; `None` on an unborn branch.
559 pub(crate) oid: Option<ObjectId>,
560}
561
562/// Read `HEAD` for the advertisement.
563pub(crate) fn head_info(repo: &Repository) -> HeadInfo {
564 HeadInfo {
565 target: crate::refs::read_symbolic_ref(repo.git_dir(), "HEAD")
566 .ok()
567 .flatten(),
568 oid: crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok(),
569 }
570}
571
572/// Write a pkt-line carrying `data` followed by a newline.
573pub(crate) fn write_line(out: &mut dyn std::io::Write, data: &str) -> Result<()> {
574 let mut buf = Vec::with_capacity(data.len() + 5);
575 crate::pkt_line::write_line_to_vec(&mut buf, data)?;
576 out.write_all(&buf)?;
577 Ok(())
578}
579
580/// Write a flush packet.
581pub(crate) fn write_flush(out: &mut dyn std::io::Write) -> Result<()> {
582 out.write_all(crate::pkt_line::FLUSH.as_bytes())?;
583 Ok(())
584}
585
586/// Read one packet, normalising a data line by dropping its trailing newline.
587pub(crate) fn read_packet(input: &mut dyn std::io::Read) -> Result<Option<Packet>> {
588 let pkt = crate::pkt_line::read_packet(&mut &mut *input)?;
589 Ok(pkt.map(|p| match p {
590 crate::pkt_line::Packet::Data(s) => Packet::Data(s.trim_end_matches('\n').to_owned()),
591 crate::pkt_line::Packet::Flush => Packet::Flush,
592 crate::pkt_line::Packet::Delim => Packet::Delim,
593 crate::pkt_line::Packet::ResponseEnd => Packet::ResponseEnd,
594 }))
595}
596
597/// A packet read from the client, with data lines already trimmed.
598#[derive(Debug, PartialEq, Eq)]
599pub(crate) enum Packet {
600 /// A data line without its trailing newline.
601 Data(String),
602 /// `0000`.
603 Flush,
604 /// `0001`.
605 Delim,
606 /// `0002`.
607 ResponseEnd,
608}
609
610/// Parse a full-width hex object id for the repository's hash algorithm.
611pub(crate) fn parse_oid(repo: &Repository, hex: &str) -> Result<ObjectId> {
612 let oid = ObjectId::from_hex(hex)
613 .map_err(|_| ServeError::Protocol(format!("invalid object id '{hex}'")))?;
614 if oid.algo() != repo.odb().hash_algo() {
615 return Err(ServeError::Protocol(format!(
616 "object id '{hex}' does not match the repository hash algorithm"
617 )));
618 }
619 Ok(oid)
620}
621
622/// Check a client's `object-format=` capability against the repository.
623pub(crate) fn check_object_format(repo: &Repository, client: Option<&str>) -> Result<()> {
624 let ours = repo.odb().hash_algo().name();
625 match client {
626 Some(name) if name != ours => Err(ServeError::ObjectFormatMismatch {
627 client: name.to_owned(),
628 repository: ours,
629 }),
630 _ => Ok(()),
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::{advertised_refs, scan_prefixes, Peel, ProtocolVersion};
637 use crate::config::environment::RepositoryOptions;
638 use crate::objects::{ObjectId, ObjectKind};
639 use crate::repo::{InitOptions, Repository};
640
641 #[test]
642 fn scan_prefixes_collapse_to_disjoint_sorted_ref_prefixes() {
643 assert_eq!(scan_prefixes(&[]), vec!["refs/"]);
644 assert_eq!(scan_prefixes(&["HEAD"]), Vec::<&str>::new());
645 assert_eq!(scan_prefixes(&["", "HEAD"]), vec!["refs/"]);
646 assert_eq!(scan_prefixes(&["re", "refs/tags/"]), vec!["refs/"]);
647 assert_eq!(
648 scan_prefixes(&[
649 "refs/tags/v1",
650 "refs/heads/",
651 "refs/tags/",
652 "refs/heads/main"
653 ]),
654 vec!["refs/heads/", "refs/tags/"]
655 );
656 assert_eq!(
657 scan_prefixes(&["refs/tags/v2", "refs/tags/v1", "refs/tags/v1"]),
658 vec!["refs/tags/v1", "refs/tags/v2"]
659 );
660 }
661
662 /// A bare repository with `main`, `feature`, an annotated tag `v1`, a hidden ref, and a
663 /// ref naming a missing object.
664 fn fixture() -> (tempfile::TempDir, Repository, ObjectId, ObjectId) {
665 let dir = tempfile::tempdir().expect("tempdir");
666 Repository::init(
667 dir.path(),
668 &InitOptions::new()
669 .with_bare(true)
670 .with_initial_branch("main"),
671 )
672 .expect("init");
673 let repo = Repository::open(dir.path(), &RepositoryOptions::empty()).expect("open");
674 let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
675 let commit = repo
676 .odb()
677 .write(
678 ObjectKind::Commit,
679 format!("tree {tree}\nauthor A <a@e> 1 +0000\ncommitter A <a@e> 1 +0000\n\nc\n")
680 .as_bytes(),
681 )
682 .expect("commit");
683 let tag = repo
684 .odb()
685 .write(
686 ObjectKind::Tag,
687 format!("object {commit}\ntype commit\ntag v1\ntagger A <a@e> 1 +0000\n\nv1\n")
688 .as_bytes(),
689 )
690 .expect("tag");
691 let missing = ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").expect("oid");
692 for (name, oid) in [
693 ("refs/heads/main", commit),
694 ("refs/heads/feature", commit),
695 ("refs/heads/broken", missing),
696 ("refs/hidden/x", commit),
697 ("refs/tags/v1", tag),
698 ] {
699 let path = dir.path().join(name);
700 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
701 std::fs::write(path, format!("{oid}\n")).expect("ref");
702 }
703 (dir, repo, commit, tag)
704 }
705
706 fn listed(refs: &[super::AdvertisedRef]) -> Vec<(&str, Option<ObjectId>)> {
707 refs.iter().map(|r| (r.name.as_str(), r.peeled)).collect()
708 }
709
710 #[cfg_attr(
711 not(feature = "full-tests"),
712 ignore = "starts system git or is heavy; run with --features full-tests"
713 )]
714 #[test]
715 fn advertised_refs_lists_requested_prefixes_and_skips_hidden() {
716 let (_dir, repo, commit, _tag) = fixture();
717 let hidden = vec!["refs/hidden".to_owned()];
718
719 let all = advertised_refs(&repo, &hidden, &[], Peel::Yes).expect("all");
720 assert_eq!(
721 listed(&all),
722 vec![
723 ("refs/heads/broken", None),
724 ("refs/heads/feature", None),
725 ("refs/heads/main", None),
726 ("refs/tags/v1", Some(commit)),
727 ]
728 );
729
730 let some = advertised_refs(
731 &repo,
732 &hidden,
733 &["refs/tags/", "refs/heads/m", "HEAD", "refs/hidden/"],
734 Peel::No,
735 )
736 .expect("prefixed");
737 assert_eq!(
738 listed(&some),
739 vec![("refs/heads/main", None), ("refs/tags/v1", None)]
740 );
741
742 let none = advertised_refs(&repo, &hidden, &["HEAD"], Peel::Yes).expect("HEAD only");
743 assert!(none.is_empty());
744 }
745
746 #[test]
747 fn git_protocol_picks_highest_version() {
748 assert_eq!(ProtocolVersion::from_git_protocol(""), ProtocolVersion::V0);
749 assert_eq!(
750 ProtocolVersion::from_git_protocol("version=1"),
751 ProtocolVersion::V1
752 );
753 assert_eq!(
754 ProtocolVersion::from_git_protocol("version=2:object-format=sha1"),
755 ProtocolVersion::V2
756 );
757 assert_eq!(
758 ProtocolVersion::from_git_protocol("version=9"),
759 ProtocolVersion::V0
760 );
761 }
762}