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