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