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