Skip to main content

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}