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 object_info;
21mod receive_pack;
22mod ref_update_policy;
23mod shallow_upload;
24mod upload_pack;
25mod upload_pack_policy;
26
27pub(crate) use shallow_upload::{compute_shallow_response, ShallowRequest};
28
29pub use receive_pack::{
30    receive_pack, receive_pack_with_hook, HookRunRecord, ReceiveHook, ReceiveOutcome,
31    ReceivePolicy, RefCommand, RefUpdateRefusal, RefUpdateResult, RejectedObject, UnpackError,
32};
33pub use ref_update_policy::{checked_out_branch, head_branch, validate_ref_update, HeadBranches};
34pub use upload_pack::upload_pack;
35
36use std::ops::ControlFlow;
37
38use crate::objects::ObjectId;
39use crate::refs::listing::PACKED_PEELS_MIN_REFS;
40use crate::refs::store::RawRef;
41use crate::refs::PackedRefPeels;
42use crate::repo::Repository;
43
44/// Errors produced while serving a fetch or a push.
45#[derive(Debug, thiserror::Error)]
46pub enum ServeError {
47    /// The peer sent something that does not follow the wire protocol.
48    #[error("protocol error: {0}")]
49    Protocol(String),
50    /// The client asked for an object that is not an advertised ref tip.
51    #[error("not our ref: {0}")]
52    NotOurRef(ObjectId),
53    /// The client requested a v2 command this server does not implement.
54    #[error("unknown command: {0}")]
55    UnknownCommand(String),
56    /// A `deepen-not` argument (`--shallow-exclude`) cannot be used as a
57    /// shallow-history exclusion point.
58    #[error("invalid deepen-not '{spec}': {reason}")]
59    InvalidDeepenNot {
60        /// The argument exactly as the client sent it.
61        spec: String,
62        /// Why it was rejected.
63        reason: DeepenNotError,
64    },
65    /// An upload-pack filter request was rejected by repository config.
66    #[error(transparent)]
67    UploadFilter(#[from] crate::upload_filter::UploadFilterError),
68    /// The client and repository disagree on the object hash algorithm.
69    #[error("object format mismatch: client wants {client}, repository uses {repository}")]
70    ObjectFormatMismatch {
71        /// Hash algorithm named by the client.
72        client: String,
73        /// Hash algorithm of the served repository.
74        repository: &'static str,
75    },
76    /// Reading or writing repository data failed.
77    #[error(transparent)]
78    Repository(#[from] crate::error::Error),
79    /// The underlying stream failed.
80    #[error(transparent)]
81    Io(#[from] std::io::Error),
82}
83
84/// Why a `deepen-not` argument was rejected; see [`ServeError::InvalidDeepenNot`].
85#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
86pub enum DeepenNotError {
87    /// The argument names neither an existing ref nor an object in the repository.
88    #[error("no such ref or object")]
89    UnknownRef,
90    /// A short name matched more than one ref under the ref lookup rules.
91    #[error("matches more than one ref")]
92    AmbiguousRef,
93    /// The ref or object id does not lead to a commit once tags are peeled.
94    #[error("does not point at a commit")]
95    NotACommit,
96}
97
98/// Result alias for serving operations.
99pub type Result<T> = std::result::Result<T, ServeError>;
100
101/// Wire protocol version requested by the client.
102///
103/// Version 1 is version 0 with an explicit `version 1` line in front of the
104/// advertisement. Version 2 replaces the ref advertisement with a capability
105/// list and command requests.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum ProtocolVersion {
108    /// The original protocol, used when the client asks for nothing else.
109    #[default]
110    V0,
111    /// Version 0 framing announced with a `version 1` line.
112    V1,
113    /// Command-based protocol version 2.
114    V2,
115}
116
117impl ProtocolVersion {
118    /// Pick the highest version named in a `GIT_PROTOCOL` style value.
119    ///
120    /// The value is a colon-separated list of `key=value` entries; only
121    /// `version=N` entries are considered. Unknown or missing versions fall
122    /// back to [`ProtocolVersion::V0`].
123    ///
124    /// # Parameters
125    ///
126    /// - `value`: the raw value, for example `version=2` or `version=2:foo=bar`.
127    ///
128    /// # Returns
129    ///
130    /// The highest supported version the value asks for.
131    #[must_use]
132    pub fn from_git_protocol(value: &str) -> Self {
133        value
134            .split(':')
135            .filter_map(|entry| entry.strip_prefix("version="))
136            .filter_map(|v| match v {
137                "2" => Some(Self::V2),
138                "1" => Some(Self::V1),
139                "0" => Some(Self::V0),
140                _ => None,
141            })
142            .max_by_key(|v| match v {
143                Self::V0 => 0,
144                Self::V1 => 1,
145                Self::V2 => 2,
146            })
147            .unwrap_or_default()
148    }
149}
150
151/// How a serving session is framed on the wire.
152#[derive(Debug, Clone, Default)]
153pub struct ServeOptions {
154    /// Protocol version the client asked for.
155    pub protocol: ProtocolVersion,
156    /// Handle exactly one request from the input and stop, without printing the
157    /// advertisement first. Smart HTTP uses this for its POST requests.
158    pub stateless_rpc: bool,
159    /// Print the ref (or capability) advertisement and stop. Smart HTTP uses
160    /// this to answer `GET info/refs`.
161    pub advertise_refs: bool,
162    /// Value for the `agent=` capability, such as `grit/0.5.0`.
163    pub agent: String,
164    /// Ref patterns hidden from the advertisement (`transfer.hideRefs` and the
165    /// service-specific `uploadpack.hideRefs` / `receive.hideRefs`).
166    pub hidden_refs: Vec<String>,
167    /// Values the client sent as v2 `server-option=<value>` capability lines on
168    /// the current command request. Cleared at the start of each v2 command and
169    /// filled before `ls-refs` / `fetch` / `object-info` handlers run.
170    pub server_options: Vec<String>,
171    /// Bundles to offer through the protocol v2 `bundle-uri` command. `None`
172    /// leaves the command out of the advertisement.
173    pub bundle_list: Option<crate::bundle_uri::BundleList>,
174    /// Where clients may upload a resumable push bundle, advertised by
175    /// receive-pack as `pushBundle=<value>`; relative values are relative to
176    /// the repository URL. `None` advertises nothing.
177    pub push_bundle: Option<String>,
178}
179
180/// Name of the receive-pack capability that advertises a resumable push
181/// endpoint ([`ServeOptions::push_bundle`]).
182pub const PUSH_BUNDLE_CAPABILITY: &str = "pushBundle";
183
184/// One ref as it appears in an advertisement.
185#[derive(Debug, Clone)]
186pub(crate) struct AdvertisedRef {
187    /// Full ref name, such as `refs/heads/main`.
188    pub(crate) name: String,
189    /// Object the ref points at.
190    pub(crate) oid: ObjectId,
191    /// For an annotated tag, the non-tag object it ultimately points at.
192    pub(crate) peeled: Option<ObjectId>,
193}
194
195/// Whether [`advertised_refs`] fills [`AdvertisedRef::peeled`].
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub(crate) enum Peel {
198    /// Leave `peeled` empty and read no objects.
199    No,
200    /// Peel annotated tags, trusting `packed-refs` peel lines where they apply.
201    Yes,
202}
203
204/// List the repository's refs in advertisement order, skipping hidden ones.
205///
206/// Only refs whose names start with one of `prefixes` are listed (every ref
207/// under `refs/` when `prefixes` is empty), and the ref store is asked for
208/// those prefixes alone, so the cost follows the refs returned rather than the
209/// refs stored. Prefixes that cannot name a ref under `refs/` (such as `HEAD`)
210/// select nothing here.
211///
212/// Refs whose names are not safe to send and symbolic refs that do not resolve
213/// are left out. As in Git, a ref naming a missing object is still advertised;
214/// with [`Peel::Yes`] it, like any ref whose tag chain cannot be read, simply
215/// gets no peeled value.
216pub(crate) fn advertised_refs(
217    repo: &Repository,
218    hidden: &[String],
219    prefixes: &[&str],
220    peel: Peel,
221) -> Result<Vec<AdvertisedRef>> {
222    let store = repo.refs();
223    let git_dir = repo.git_dir();
224    let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
225    let mut listed: Vec<(String, ObjectId)> = Vec::new();
226    for prefix in scan_prefixes(prefixes) {
227        store.for_each_ref(prefix, &mut |entry| {
228            let name = entry.name.as_str();
229            if !name.starts_with(prefix)
230                || !crate::refs::is_valid_fetch_advertised_ref(name)
231                || crate::hide_refs::ref_is_hidden(name, name, hidden)
232                || (linked_worktree
233                    && !crate::worktree_ref::ref_visible_from_worktree(git_dir, name))
234            {
235                return ControlFlow::Continue(());
236            }
237            let oid = match &entry.value {
238                RawRef::Direct(oid) => Some(*oid),
239                RawRef::Symbolic(_) => entry.peeled.or_else(|| store.resolve(name).ok()),
240            };
241            if let Some(oid) = oid {
242                listed.push((entry.name.clone(), oid));
243            }
244            ControlFlow::Continue(())
245        })?;
246    }
247    if !listed.is_sorted_by(|a, b| a.0 < b.0) {
248        listed.sort_by(|a, b| a.0.cmp(&b.0));
249        listed.dedup_by(|later, first| later.0 == first.0);
250    }
251
252    if peel == Peel::No {
253        return Ok(listed
254            .into_iter()
255            .map(|(name, oid)| AdvertisedRef {
256                name,
257                oid,
258                peeled: None,
259            })
260            .collect());
261    }
262    let odb = repo.odb();
263    // A malformed `packed-refs` only loses the shortcut; peeling falls back to the objects.
264    let packed = (listed.len() >= PACKED_PEELS_MIN_REFS)
265        .then(|| PackedRefPeels::for_store(store, git_dir).ok())
266        .flatten();
267    Ok(odb.with_pack_read_context(|| {
268        listed
269            .into_iter()
270            .map(|(name, oid)| {
271                let peeled = packed
272                    .as_ref()
273                    .and_then(|p| p.peeled(&name, &oid))
274                    .unwrap_or_else(|| odb.peel_tag_target(oid).ok().flatten());
275                AdvertisedRef { name, oid, peeled }
276            })
277            .collect()
278    }))
279}
280
281/// Ref-store prefixes to list so that every ref under `refs/` starting with one
282/// of `requested` is visited exactly once.
283///
284/// No request means all of `refs/`. A request shorter than `refs/` that it
285/// begins (`""`, `"re"`) widens to `refs/`; one outside it selects nothing.
286/// Prefixes covered by a shorter one are dropped, which leaves the result
287/// sorted and disjoint: listing each in turn yields names in sorted order.
288fn scan_prefixes<'a>(requested: &[&'a str]) -> Vec<&'a str> {
289    const ALL: &str = "refs/";
290    if requested.is_empty() {
291        return vec![ALL];
292    }
293    let mut scan: Vec<&str> = requested
294        .iter()
295        .filter_map(|p| {
296            if p.starts_with(ALL) {
297                Some(*p)
298            } else {
299                ALL.starts_with(p).then_some(ALL)
300            }
301        })
302        .collect();
303    scan.sort_unstable();
304    scan.dedup_by(|later, kept| later.starts_with(*kept));
305    scan
306}
307
308/// Where `HEAD` points: its symbolic target (if any) and resolved object (if born).
309pub(crate) struct HeadInfo {
310    /// Ref `HEAD` names, such as `refs/heads/main`, when `HEAD` is symbolic.
311    pub(crate) target: Option<String>,
312    /// Object `HEAD` resolves to; `None` on an unborn branch.
313    pub(crate) oid: Option<ObjectId>,
314}
315
316/// Read `HEAD` for the advertisement.
317pub(crate) fn head_info(repo: &Repository) -> HeadInfo {
318    HeadInfo {
319        target: crate::refs::read_symbolic_ref(repo.git_dir(), "HEAD")
320            .ok()
321            .flatten(),
322        oid: crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok(),
323    }
324}
325
326/// Write a pkt-line carrying `data` followed by a newline.
327pub(crate) fn write_line(out: &mut dyn std::io::Write, data: &str) -> Result<()> {
328    let mut buf = Vec::with_capacity(data.len() + 5);
329    crate::pkt_line::write_line_to_vec(&mut buf, data)?;
330    out.write_all(&buf)?;
331    Ok(())
332}
333
334/// Write a flush packet.
335pub(crate) fn write_flush(out: &mut dyn std::io::Write) -> Result<()> {
336    out.write_all(crate::pkt_line::FLUSH.as_bytes())?;
337    Ok(())
338}
339
340/// Read one packet, normalising a data line by dropping its trailing newline.
341pub(crate) fn read_packet(input: &mut dyn std::io::Read) -> Result<Option<Packet>> {
342    let pkt = crate::pkt_line::read_packet(&mut &mut *input)?;
343    Ok(pkt.map(|p| match p {
344        crate::pkt_line::Packet::Data(s) => Packet::Data(s.trim_end_matches('\n').to_owned()),
345        crate::pkt_line::Packet::Flush => Packet::Flush,
346        crate::pkt_line::Packet::Delim => Packet::Delim,
347        crate::pkt_line::Packet::ResponseEnd => Packet::ResponseEnd,
348    }))
349}
350
351/// A packet read from the client, with data lines already trimmed.
352#[derive(Debug, PartialEq, Eq)]
353pub(crate) enum Packet {
354    /// A data line without its trailing newline.
355    Data(String),
356    /// `0000`.
357    Flush,
358    /// `0001`.
359    Delim,
360    /// `0002`.
361    ResponseEnd,
362}
363
364/// Parse a full-width hex object id for the repository's hash algorithm.
365pub(crate) fn parse_oid(repo: &Repository, hex: &str) -> Result<ObjectId> {
366    let oid = ObjectId::from_hex(hex)
367        .map_err(|_| ServeError::Protocol(format!("invalid object id '{hex}'")))?;
368    if oid.algo() != repo.odb().hash_algo() {
369        return Err(ServeError::Protocol(format!(
370            "object id '{hex}' does not match the repository hash algorithm"
371        )));
372    }
373    Ok(oid)
374}
375
376/// Check a client's `object-format=` capability against the repository.
377pub(crate) fn check_object_format(repo: &Repository, client: Option<&str>) -> Result<()> {
378    let ours = repo.odb().hash_algo().name();
379    match client {
380        Some(name) if name != ours => Err(ServeError::ObjectFormatMismatch {
381            client: name.to_owned(),
382            repository: ours,
383        }),
384        _ => Ok(()),
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::{advertised_refs, scan_prefixes, Peel, ProtocolVersion};
391    use crate::config::environment::RepositoryOptions;
392    use crate::objects::{ObjectId, ObjectKind};
393    use crate::repo::{InitOptions, Repository};
394
395    #[test]
396    fn scan_prefixes_collapse_to_disjoint_sorted_ref_prefixes() {
397        assert_eq!(scan_prefixes(&[]), vec!["refs/"]);
398        assert_eq!(scan_prefixes(&["HEAD"]), Vec::<&str>::new());
399        assert_eq!(scan_prefixes(&["", "HEAD"]), vec!["refs/"]);
400        assert_eq!(scan_prefixes(&["re", "refs/tags/"]), vec!["refs/"]);
401        assert_eq!(
402            scan_prefixes(&[
403                "refs/tags/v1",
404                "refs/heads/",
405                "refs/tags/",
406                "refs/heads/main"
407            ]),
408            vec!["refs/heads/", "refs/tags/"]
409        );
410        assert_eq!(
411            scan_prefixes(&["refs/tags/v2", "refs/tags/v1", "refs/tags/v1"]),
412            vec!["refs/tags/v1", "refs/tags/v2"]
413        );
414    }
415
416    /// A bare repository with `main`, `feature`, an annotated tag `v1`, a hidden ref, and a
417    /// ref naming a missing object.
418    fn fixture() -> (tempfile::TempDir, Repository, ObjectId, ObjectId) {
419        let dir = tempfile::tempdir().expect("tempdir");
420        Repository::init(
421            dir.path(),
422            &InitOptions::new()
423                .with_bare(true)
424                .with_initial_branch("main"),
425        )
426        .expect("init");
427        let repo = Repository::open(dir.path(), &RepositoryOptions::empty()).expect("open");
428        let tree = repo.odb().write(ObjectKind::Tree, b"").expect("tree");
429        let commit = repo
430            .odb()
431            .write(
432                ObjectKind::Commit,
433                format!("tree {tree}\nauthor A <a@e> 1 +0000\ncommitter A <a@e> 1 +0000\n\nc\n")
434                    .as_bytes(),
435            )
436            .expect("commit");
437        let tag = repo
438            .odb()
439            .write(
440                ObjectKind::Tag,
441                format!("object {commit}\ntype commit\ntag v1\ntagger A <a@e> 1 +0000\n\nv1\n")
442                    .as_bytes(),
443            )
444            .expect("tag");
445        let missing = ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").expect("oid");
446        for (name, oid) in [
447            ("refs/heads/main", commit),
448            ("refs/heads/feature", commit),
449            ("refs/heads/broken", missing),
450            ("refs/hidden/x", commit),
451            ("refs/tags/v1", tag),
452        ] {
453            let path = dir.path().join(name);
454            std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
455            std::fs::write(path, format!("{oid}\n")).expect("ref");
456        }
457        (dir, repo, commit, tag)
458    }
459
460    fn listed(refs: &[super::AdvertisedRef]) -> Vec<(&str, Option<ObjectId>)> {
461        refs.iter().map(|r| (r.name.as_str(), r.peeled)).collect()
462    }
463
464    #[cfg_attr(
465        not(feature = "full-tests"),
466        ignore = "starts system git or is heavy; run with --features full-tests"
467    )]
468    #[test]
469    fn advertised_refs_lists_requested_prefixes_and_skips_hidden() {
470        let (_dir, repo, commit, _tag) = fixture();
471        let hidden = vec!["refs/hidden".to_owned()];
472
473        let all = advertised_refs(&repo, &hidden, &[], Peel::Yes).expect("all");
474        assert_eq!(
475            listed(&all),
476            vec![
477                ("refs/heads/broken", None),
478                ("refs/heads/feature", None),
479                ("refs/heads/main", None),
480                ("refs/tags/v1", Some(commit)),
481            ]
482        );
483
484        let some = advertised_refs(
485            &repo,
486            &hidden,
487            &["refs/tags/", "refs/heads/m", "HEAD", "refs/hidden/"],
488            Peel::No,
489        )
490        .expect("prefixed");
491        assert_eq!(
492            listed(&some),
493            vec![("refs/heads/main", None), ("refs/tags/v1", None)]
494        );
495
496        let none = advertised_refs(&repo, &hidden, &["HEAD"], Peel::Yes).expect("HEAD only");
497        assert!(none.is_empty());
498    }
499
500    #[test]
501    fn git_protocol_picks_highest_version() {
502        assert_eq!(ProtocolVersion::from_git_protocol(""), ProtocolVersion::V0);
503        assert_eq!(
504            ProtocolVersion::from_git_protocol("version=1"),
505            ProtocolVersion::V1
506        );
507        assert_eq!(
508            ProtocolVersion::from_git_protocol("version=2:object-format=sha1"),
509            ProtocolVersion::V2
510        );
511        assert_eq!(
512            ProtocolVersion::from_git_protocol("version=9"),
513            ProtocolVersion::V0
514        );
515    }
516}