Skip to main content

grit_lib/serve/
receive_pack.rs

1//! Accepting pushes (`receive-pack`).
2
3use std::collections::{HashMap, HashSet};
4use std::io::{BufReader, BufWriter, Read, Write};
5
6use super::connectivity::{PushedObjects, Unconnected};
7
8use super::{
9    check_object_format, for_each_advertised_ref, parse_oid, read_packet, ref_update_policy,
10    write_flush, write_line, Packet, Peel, ProtocolVersion, Result, ServeError, ServeOptions,
11};
12use crate::config::ConfigSet;
13use crate::fsck_msg_policy::{ObjectFsckPolicy, SkipListError};
14use crate::fsck_standalone::FsckError;
15use crate::hooks::{run_hook_in_git_dir, HookResult};
16use crate::index_pack::{install_pack_path, IngestPackOptions};
17use crate::objects::pack_stream_scan::{PackStreamScanError, PackStreamScanner};
18use crate::objects::{ObjectId, ObjectKind};
19use crate::odb::Odb;
20use crate::parallel::{try_par_map, ParallelHashError, ParallelOp};
21use crate::progress::{Cancel, CancelSignal, NeverCancel};
22use crate::receive_pack::{max_input_size_from_config, should_use_unpack_objects};
23use crate::receive_quarantine::ReceiveQuarantine;
24use crate::repo::Repository;
25use crate::shallow::{add_shallow_boundaries, load_shallow_boundaries, PushShallowInfo};
26use crate::unpack_objects::{unpack_objects, UnpackOptions};
27
28/// Bytes read from the client per call while spooling a pushed pack.
29const SPOOL_CHUNK: usize = 256 * 1024;
30
31/// Capabilities offered in the receive-pack advertisement, before the
32/// per-repository ones.
33const CAPABILITIES: &[&str] = &[
34    "report-status",
35    "report-status-v2",
36    "delete-refs",
37    "side-band-64k",
38    "quiet",
39    "atomic",
40    "ofs-delta",
41];
42
43/// Which pushed objects and ref updates the server refuses.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ReceivePolicy {
46    /// Refuse branch (`refs/heads/*`) updates that would discard commits
47    /// (`receive.denyNonFastForwards`). Other refs are not checked.
48    pub deny_non_fast_forwards: bool,
49    /// Refuse ref deletions (`receive.denyDeletes`).
50    pub deny_deletes: bool,
51    /// In a repository with a working tree, refuse to update or delete the
52    /// branch `HEAD` points at (`receive.denyCurrentBranch`).
53    pub deny_current_branch: bool,
54    /// Refuse to delete the branch `HEAD` points at, in a bare repository as
55    /// well as one with a working tree (`receive.denyDeleteCurrent`).
56    pub deny_delete_current: bool,
57    /// Check every pushed commit, tree and tag with this policy before any of
58    /// them enters the repository (`receive.fsckObjects`). One refused object
59    /// refuses the whole push with [`UnpackError::Rejected`]. `None` skips the
60    /// check.
61    pub fsck_objects: Option<ObjectFsckPolicy>,
62    /// Accept pushes from a shallow client that would make this repository
63    /// shallow at one of the client's boundaries (`receive.shallowUpdate`).
64    /// The new boundaries are added to the repository's `shallow` file.
65    /// When false, such updates are refused with
66    /// [`RefUpdateRefusal::ShallowUpdateNotAllowed`]; pushes whose history the
67    /// repository already holds below the client's boundaries are accepted
68    /// either way.
69    pub shallow_update: bool,
70}
71
72impl Default for ReceivePolicy {
73    fn default() -> Self {
74        Self {
75            deny_non_fast_forwards: false,
76            deny_deletes: false,
77            deny_current_branch: true,
78            deny_delete_current: true,
79            fsck_objects: None,
80            shallow_update: false,
81        }
82    }
83}
84
85impl ReceivePolicy {
86    /// Read the policy from the `receive.*` configuration keys.
87    ///
88    /// `receive.denyCurrentBranch` accepts Git's values: `refuse`, `true` or
89    /// unset refuse the update; `ignore`, `warn`, `false` and `updateInstead`
90    /// allow it (this server does not update the working tree).
91    /// `receive.denyDeleteCurrent` is the same, except that `updateInstead`
92    /// refuses: there is no working tree to update for a deleted branch.
93    ///
94    /// Pushed objects are checked when `receive.fsckObjects` is true, or when it
95    /// is unset and `transfer.fsckObjects` is true. A value that is not a
96    /// boolean turns the check on rather than silently leaving it off. The
97    /// check uses `receive.fsck.<msg-id>` overrides and `receive.fsck.skipList`
98    /// (see [`ObjectFsckPolicy::from_config`]).
99    ///
100    /// # Parameters
101    ///
102    /// - `config`: the repository's loaded configuration.
103    ///
104    /// # Returns
105    ///
106    /// The policy, with defaults for unset or unparseable keys.
107    #[must_use]
108    pub fn from_config(config: &ConfigSet) -> Self {
109        let flag = |key: &str| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(false);
110        // Whether a `receive.deny*` key is set to a value that lets the update through.
111        let allows = |key: &str, update_instead_allows: bool| {
112            config.get(key).is_some_and(|v| {
113                let v = v.to_ascii_lowercase();
114                matches!(v.as_str(), "ignore" | "warn" | "false" | "no" | "off" | "0")
115                    || (update_instead_allows && v == "updateinstead")
116            })
117        };
118        let explicit = |key: &str| config.get_bool(key).map(|r| r.unwrap_or(true));
119        let fsck = explicit("receive.fsckObjects")
120            .or_else(|| explicit("transfer.fsckObjects"))
121            .unwrap_or(false);
122        Self {
123            deny_non_fast_forwards: flag("receive.denyNonFastForwards"),
124            deny_deletes: flag("receive.denyDeletes"),
125            deny_current_branch: !allows("receive.denyCurrentBranch", true),
126            deny_delete_current: !allows("receive.denyDeleteCurrent", false),
127            fsck_objects: fsck.then(|| ObjectFsckPolicy::from_config(config, "receive.fsck")),
128            shallow_update: flag("receive.shallowUpdate"),
129        }
130    }
131}
132
133/// A pushed object refused by [`ReceivePolicy::fsck_objects`].
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct RejectedObject {
136    /// The refused object.
137    pub oid: ObjectId,
138    /// Its type.
139    pub kind: ObjectKind,
140    /// The error-level problem found in it.
141    pub error: FsckError,
142}
143
144impl std::fmt::Display for RejectedObject {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        write!(
147            f,
148            "{} {} refused: {}",
149            self.kind,
150            self.oid.to_hex(),
151            self.error.report_line()
152        )
153    }
154}
155
156/// One `<old> <new> <ref>` update requested by a pushing client.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct RefCommand {
159    /// Full name of the ref to change, such as `refs/heads/main`.
160    pub refname: String,
161    /// Value the client expects the ref to have now; `None` to create it.
162    pub old: Option<ObjectId>,
163    /// Value the client asks for; `None` to delete the ref.
164    pub new: Option<ObjectId>,
165}
166
167impl RefCommand {
168    fn refused(&self, reason: RefUpdateRefusal) -> RefUpdateResult {
169        RefUpdateResult {
170            refname: self.refname.clone(),
171            old: self.old,
172            new: self.new,
173            error: Some(reason),
174        }
175    }
176
177    fn accepted(&self) -> RefUpdateResult {
178        RefUpdateResult {
179            refname: self.refname.clone(),
180            old: self.old,
181            new: self.new,
182            error: None,
183        }
184    }
185}
186
187/// Why the server refused one ref update.
188///
189/// The [`Display`](std::fmt::Display) text is the reason sent to the client on
190/// the update's `ng <ref> <reason>` status line, which Git clients show next to
191/// the rejected ref.
192#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
193#[non_exhaustive]
194pub enum RefUpdateRefusal {
195    /// The ref name is outside `refs/` or not a valid ref name.
196    #[error("funny refname")]
197    InvalidRefName,
198    /// The ref is hidden by `receive.hideRefs` / `transfer.hideRefs`.
199    #[error("deny updating a hidden ref")]
200    HiddenRef,
201    /// The ref no longer has the value the client expected.
202    #[error("stale info")]
203    StaleInfo,
204    /// Deletions are refused (`receive.denyDeletes`).
205    #[error("deletion prohibited")]
206    DeletionProhibited,
207    /// The update would delete the branch `HEAD` points at
208    /// (`receive.denyDeleteCurrent`).
209    #[error("deletion of the current branch prohibited")]
210    CurrentBranchDeletion,
211    /// The update would move the checked-out branch
212    /// (`receive.denyCurrentBranch`).
213    #[error("branch is currently checked out")]
214    CurrentBranch,
215    /// The new value is not in the repository or the pushed pack.
216    #[error("missing necessary objects")]
217    MissingObjects,
218    /// The new value is there, but an object its history reaches (below what the
219    /// repository's refs already reached) is in neither the push nor the
220    /// repository, and no promisor remote of the repository promises it. The new
221    /// value may have been in the repository before the push: objects a refused
222    /// ref leaves behind do not make its history complete. Git clients see the
223    /// same reason as for [`Self::MissingObjects`].
224    #[error("missing necessary objects")]
225    Unconnected {
226        /// The first missing object found.
227        missing: ObjectId,
228        /// The object that names it.
229        referrer: ObjectId,
230    },
231    /// The branch update would discard commits (`receive.denyNonFastForwards`).
232    #[error("non-fast-forward")]
233    NonFastForward,
234    /// A branch update under `receive.denyNonFastForwards` names a non-commit
235    /// as its old or new value.
236    #[error("bad ref")]
237    BadRef,
238    /// Under `receive.denyNonFastForwards`, the ancestry walk between the old
239    /// and new branch tips failed, so the update could not be shown to be a
240    /// fast-forward.
241    #[error("unable to verify fast-forward")]
242    FastForwardCheckFailed,
243    /// The pushed history ends at a boundary of the shallow client that this
244    /// repository does not hold the ancestry of, and
245    /// [`ReceivePolicy::shallow_update`] is off.
246    #[error("shallow update not allowed")]
247    ShallowUpdateNotAllowed,
248    /// The pushed pack could not be stored; see
249    /// [`ReceiveOutcome::unpack_error`].
250    #[error("unpacker error")]
251    UnpackerError,
252    /// Another update in an `atomic` push was refused, so this one was not
253    /// applied either.
254    #[error("atomic transaction failed")]
255    AtomicTransactionFailed,
256    /// The `pre-receive` hook script exited non-zero.
257    #[error("pre-receive hook declined")]
258    PreReceiveHookDeclined,
259    /// The `update` hook script exited non-zero for this ref.
260    #[error("hook declined")]
261    UpdateHookDeclined,
262    /// A [`ReceiveHook`] refused the update with this reason.
263    #[error("{0}")]
264    Policy(String),
265    /// The ref is a served repository's target branch, which changes only
266    /// through the server's own operations (snapshot checkouts' integrate and
267    /// slices), never by a push.
268    #[error("{target} is the target branch; it changes only through integrate")]
269    TargetBranch {
270        /// The target branch's name, without `refs/heads/`.
271        target: String,
272    },
273    /// Writing the ref failed after every check passed.
274    #[error("failed to update ref: {0}")]
275    RefStore(String),
276}
277
278/// The result of one requested ref update.
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct RefUpdateResult {
281    /// Full name of the ref the client asked to change.
282    pub refname: String,
283    /// Value the client expected the ref to have; `None` for a create.
284    pub old: Option<ObjectId>,
285    /// Value the client asked for; `None` for a delete.
286    pub new: Option<ObjectId>,
287    /// Why the update was refused, or `None` when it was applied.
288    pub error: Option<RefUpdateRefusal>,
289}
290
291/// Why a pushed pack could not be stored.
292///
293/// The [`Display`](std::fmt::Display) text is sent to the client on the
294/// `unpack <reason>` status line. Every requested update is then refused with
295/// [`RefUpdateRefusal::UnpackerError`].
296#[derive(Debug, thiserror::Error)]
297#[non_exhaustive]
298pub enum UnpackError {
299    /// The client stopped sending before the pack was complete.
300    #[error("unexpected end of pack stream")]
301    Truncated,
302    /// The pack is larger than `receive.maxInputSize`.
303    #[error("pack exceeds maximum input size of {limit} bytes")]
304    TooLarge {
305        /// The configured limit in bytes.
306        limit: u64,
307    },
308    /// The stream is not a well-formed pack.
309    #[error(transparent)]
310    Malformed(#[from] PackStreamScanError),
311    /// The pack arrived but its objects could not be stored (bad checksum,
312    /// corrupt object, failed connectivity check, ...).
313    #[error(transparent)]
314    Ingest(Box<crate::error::Error>),
315    /// A pushed object failed [`ReceivePolicy::fsck_objects`].
316    #[error("{0}")]
317    Rejected(RejectedObject),
318    /// The `receive.fsck.skipList` file could not be loaded, so no object
319    /// could be checked.
320    #[error(transparent)]
321    FsckSkipList(#[from] SkipListError),
322    /// Reading the client stream or writing the spooled pack failed.
323    #[error(transparent)]
324    Io(#[from] std::io::Error),
325    /// The session was stopped by a cancel signal while reading the pack.
326    #[error("cancelled")]
327    Cancelled,
328}
329
330impl From<crate::error::Error> for UnpackError {
331    fn from(err: crate::error::Error) -> Self {
332        Self::Ingest(Box::new(err))
333    }
334}
335
336/// In-process checks an embedding server runs on every push.
337///
338/// These run in addition to [`ReceivePolicy`] and the repository's hook scripts,
339/// so a server can refuse updates from Rust code without installing a script.
340/// Both methods default to accepting; implement the ones you need. A closure
341/// `Fn(&RefCommand, &Odb) -> Result<(), RefUpdateRefusal>` implements the
342/// per-ref [`Self::update`] check, and `()` accepts everything.
343///
344/// Refusals are usually [`RefUpdateRefusal::Policy`] with a reason the client
345/// will see.
346///
347/// Push options from the client are passed in [`ReceiveHookContext`] to
348/// [`Self::pre_receive_ctx`] and [`Self::update_ctx`]; the default methods ignore
349/// them and call [`Self::pre_receive`] and [`Self::update`].
350pub trait ReceiveHook {
351    /// Inspect the whole push before any ref changes, like a `pre-receive` hook.
352    ///
353    /// Runs before the `pre-receive` script, while the pushed objects are still
354    /// in quarantine.
355    ///
356    /// # Parameters
357    ///
358    /// - `commands`: the updates that passed the built-in checks, in request
359    ///   order.
360    /// - `odb`: an object database that sees the repository's objects and the
361    ///   quarantined pushed ones.
362    ///
363    /// # Errors
364    ///
365    /// Returning a refusal refuses every update in `commands` with it.
366    fn pre_receive(
367        &self,
368        commands: &[RefCommand],
369        odb: &Odb,
370    ) -> std::result::Result<(), RefUpdateRefusal> {
371        let _ = (commands, odb);
372        Ok(())
373    }
374
375    /// Like [`Self::pre_receive`], with push options from the client request.
376    fn pre_receive_ctx(
377        &self,
378        commands: &[RefCommand],
379        odb: &Odb,
380        ctx: &ReceiveHookContext<'_>,
381    ) -> std::result::Result<(), RefUpdateRefusal> {
382        let _ = ctx;
383        self.pre_receive(commands, odb)
384    }
385
386    /// Inspect one update, like an `update` hook.
387    ///
388    /// Runs before the `update` script for the same ref, after the pushed
389    /// objects were moved into the repository.
390    ///
391    /// # Parameters
392    ///
393    /// - `command`: the update to check.
394    /// - `odb`: the repository's object database, including the pushed objects.
395    ///
396    /// # Errors
397    ///
398    /// Returning a refusal refuses this update only (or the whole push when the
399    /// client asked for an `atomic` push).
400    fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
401        let _ = (command, odb);
402        Ok(())
403    }
404
405    /// Like [`Self::update`], with push options from the client request.
406    fn update_ctx(
407        &self,
408        command: &RefCommand,
409        odb: &Odb,
410        ctx: &ReceiveHookContext<'_>,
411    ) -> std::result::Result<(), RefUpdateRefusal> {
412        let _ = ctx;
413        self.update(command, odb)
414    }
415
416    /// What to tell the pushing client once every update has been decided,
417    /// as a hook script's output would: Git clients show each line as
418    /// `remote: <line>`.
419    ///
420    /// The text goes out on the progress side band just before the status
421    /// report, so only to clients that asked for a side band. The default
422    /// sends nothing.
423    ///
424    /// # Parameters
425    ///
426    /// - `updates`: every requested update with its result, in request order.
427    ///
428    /// # Returns
429    ///
430    /// The text, or `None` for nothing.
431    fn message(&self, updates: &[RefUpdateResult]) -> Option<String> {
432        let _ = updates;
433        None
434    }
435}
436
437/// Push options and other session context for [`ReceiveHook`].
438#[derive(Debug, Clone, Copy, Default)]
439pub struct ReceiveHookContext<'a> {
440    /// Values the client sent after the command list (`push-options` capability).
441    pub push_options: &'a [String],
442}
443
444impl ReceiveHook for () {}
445
446impl<F> ReceiveHook for F
447where
448    F: Fn(&RefCommand, &Odb) -> std::result::Result<(), RefUpdateRefusal>,
449{
450    fn update(&self, command: &RefCommand, odb: &Odb) -> std::result::Result<(), RefUpdateRefusal> {
451        self(command, odb)
452    }
453}
454
455/// One server-side hook invocation during receive-pack.
456#[derive(Debug, Clone, PartialEq, Eq)]
457pub struct HookRunRecord {
458    /// Hook name (`pre-receive`, `update`, `post-receive`, or `post-update`).
459    pub name: String,
460    /// Whether an executable hook ran (missing hooks are skipped).
461    pub ran: bool,
462    /// Exit code when [`Self::ran`] is true.
463    pub exit_code: Option<i32>,
464}
465
466/// What a push session did.
467#[derive(Debug, Default)]
468pub struct ReceiveOutcome {
469    /// Why the pushed pack could not be stored, when it could not.
470    pub unpack_error: Option<UnpackError>,
471    /// One entry per ref update the client requested, in request order.
472    pub updates: Vec<RefUpdateResult>,
473    /// Hook runs performed for this push, in execution order.
474    pub hooks: Vec<HookRunRecord>,
475    /// Push options delivered to hooks via `GIT_PUSH_OPTION_*`.
476    pub push_options: Vec<String>,
477}
478
479/// Serve one push session: advertise refs, read the ref updates and pack, store
480/// the objects, apply the updates, and report the result to the client.
481///
482/// The same as [`receive_pack_with_hook`] with no in-process [`ReceiveHook`].
483///
484/// # Errors
485///
486/// See [`receive_pack_with_hook`].
487pub fn receive_pack(
488    repo: &Repository,
489    input: &mut dyn Read,
490    output: &mut dyn Write,
491    opts: &ServeOptions,
492    policy: &ReceivePolicy,
493) -> Result<ReceiveOutcome> {
494    receive_pack_with_hook(repo, input, output, opts, policy, &())
495}
496
497/// Serve one push session, running `hook` alongside the hook scripts.
498///
499/// The pushed pack is streamed into a temp file inside a quarantine object
500/// directory, so memory use does not grow with the pack size. Its objects are
501/// then stored via [`unpack_objects`] for packs with fewer objects than
502/// `receive.unpackLimit` and installed as a pack with an index otherwise,
503/// matching Git receive-pack. On that unpack path, blobs larger than 1 MiB are
504/// inflated and written as loose objects in fixed-size chunks, so peak memory
505/// stays bounded rather than holding the full inflated payload. Updates are checked against `policy` and
506/// against the value the client expected each ref to have; with the `atomic`
507/// capability either every update is applied or none is. Server-side hooks run
508/// through the repository [`CommandRunner`](crate::command_runner::CommandRunner)
509/// (`pre-receive`, `update`, `post-receive`, `post-update`) with push options and
510/// quarantined object ingestion matching Git receive-pack. `hook` runs just
511/// before the matching script: [`ReceiveHook::pre_receive`] before
512/// `pre-receive` and [`ReceiveHook::update`] before each `update`; its
513/// [`ReceiveHook::message`] reaches a client that asked for a side band just
514/// before the status report.
515///
516/// The pack is read only up to its trailing checksum, never to end of input:
517/// a push client keeps its side open until it has read the status report.
518///
519/// A shallow client announces its boundaries with `shallow <oid>` lines before
520/// the commands. Their parents are not required when the pushed objects are
521/// checked. An update whose history ends at such a boundary that this
522/// repository's refs do not already reach would make the repository shallow:
523/// it is refused with [`RefUpdateRefusal::ShallowUpdateNotAllowed`] unless
524/// [`ReceivePolicy::shallow_update`] is on, in which case the boundaries are
525/// added to the `shallow` file before the ref is written and hook scripts see
526/// the client's boundaries through `GIT_SHALLOW_FILE`.
527///
528/// # Parameters
529///
530/// - `repo`: the repository receiving the push.
531/// - `input`: the client's command list and pack.
532/// - `output`: where the advertisement and status report are written.
533/// - `opts`: framing, protocol version, agent string, and hidden refs. Protocol
534///   v2 is not defined for pushes, so it is served as v0.
535/// - `policy`: which updates to refuse.
536/// - `hook`: in-process checks; pass `&()` for none.
537///
538/// # Returns
539///
540/// What was stored and which updates were applied. A pack that could not be
541/// stored ([`ReceiveOutcome::unpack_error`]) and refused updates
542/// ([`RefUpdateResult::error`]) are reported to the client and in the outcome;
543/// they are not errors.
544///
545/// # Errors
546///
547/// Returns [`ServeError::Protocol`] for a malformed command list, and I/O or
548/// repository errors from writing the response, creating the quarantine, or
549/// moving the received objects into the repository.
550pub fn receive_pack_with_hook(
551    repo: &Repository,
552    input: &mut dyn Read,
553    output: &mut dyn Write,
554    opts: &ServeOptions,
555    policy: &ReceivePolicy,
556    hook: &dyn ReceiveHook,
557) -> Result<ReceiveOutcome> {
558    receive_pack_with_hook_cancellable(repo, input, output, opts, policy, hook, &NeverCancel)
559}
560
561/// Serve one push session with a cancel signal, running `hook` alongside hook scripts.
562///
563/// Same as [`receive_pack_with_hook`], but stops without moving refs when `cancel`
564/// fires while reading the pack or before updates are applied.
565///
566/// # Errors
567///
568/// Returns [`ServeError::Cancelled`] when `cancel` is already set or fires during
569/// the session before ref updates are committed.
570pub fn receive_pack_with_hook_cancellable(
571    repo: &Repository,
572    input: &mut dyn Read,
573    output: &mut dyn Write,
574    opts: &ServeOptions,
575    policy: &ReceivePolicy,
576    hook: &dyn ReceiveHook,
577    cancel: &dyn Cancel,
578) -> Result<ReceiveOutcome> {
579    let stop = CancelSignal::new(cancel);
580    stop.poll().map_err(|_| ServeError::Cancelled)?;
581    if opts.advertise_refs || !opts.stateless_rpc {
582        if opts.protocol == ProtocolVersion::V1 {
583            write_line(output, "version 1")?;
584        }
585        write_advertisement(repo, output, opts)?;
586        output.flush()?;
587        if opts.advertise_refs {
588            return Ok(ReceiveOutcome::default());
589        }
590    }
591
592    let Some(request) = read_commands(repo, input)? else {
593        return Ok(ReceiveOutcome::default());
594    };
595
596    let cfg = repo
597        .config()
598        .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
599    let max_input = max_input_size_from_config(cfg.as_ref());
600    let sideband = request.caps.contains("side-band-64k");
601
602    let needs_pack = request.commands.iter().any(|c| c.new.is_some());
603    let mut quarantine = if needs_pack {
604        Some(ReceiveQuarantine::create(repo.odb(), repo.git_dir())?)
605    } else {
606        None
607    };
608
609    let receiver_grafts = if request.shallow.is_empty() {
610        HashSet::new()
611    } else {
612        load_shallow_boundaries(repo.git_dir())
613    };
614    let mut shallow_info =
615        PushShallowInfo::classify(&request.shallow, repo.odb(), &receiver_grafts)?;
616    let client_boundaries: HashSet<ObjectId> = request.shallow.iter().copied().collect();
617
618    // A partial repository (one offloaded to a large-object promisor, say) lacks
619    // the objects its promisor remotes have; a pushed tree may still name them.
620    let promises = if crate::promisor::repo_treats_promisor_packs(repo.git_dir(), cfg.as_ref()) {
621        Some(super::promised_objects::PromisorObjects::new(repo)?)
622    } else {
623        None
624    };
625    // What the refs reached before the push: the connectivity walk stops there.
626    let existing = if needs_pack {
627        existing_ref_values(repo)?
628    } else {
629        HashSet::new()
630    };
631    // New tips that reach a missing object: refused below.
632    let mut unconnected: HashMap<ObjectId, Unconnected> = HashMap::new();
633    let unpack_error = match quarantine.as_ref() {
634        Some(q) => {
635            let odb = q.odb();
636            spool_push_pack(input, &odb, max_input, &stop)
637                .and_then(|pack| {
638                    ingest_spooled_pack(
639                        &odb,
640                        pack,
641                        cfg.as_ref(),
642                        &client_boundaries,
643                        promises.as_ref(),
644                    )
645                })
646                .and_then(|indexed| {
647                    if let Some(fsck) = &policy.fsck_objects {
648                        check_received_objects(repo, &odb, indexed.as_ref(), cfg.as_ref(), fsck)?;
649                    }
650                    // Every push, loose or packed (even empty): objects the
651                    // repository already had are not proof of a complete history.
652                    let received = match indexed {
653                        Some(received) => received,
654                        None => odb
655                            .enumerate_local_loose_objects()?
656                            .into_iter()
657                            .map(|(oid, _)| oid)
658                            .collect(),
659                    };
660                    unconnected = unconnected_tips(
661                        repo,
662                        &odb,
663                        &received,
664                        &existing,
665                        &request.commands,
666                        &client_boundaries,
667                        promises.as_ref(),
668                    )?;
669                    Ok(())
670                })
671                .err()
672        }
673        None => None,
674    };
675    if matches!(unpack_error, Some(UnpackError::Cancelled)) {
676        return Err(ServeError::Cancelled);
677    }
678
679    let shallow = match quarantine.as_ref() {
680        Some(q) if unpack_error.is_none() => {
681            shallow_info.retain_received(&q.odb())?;
682            ShallowPlan::new(
683                repo,
684                &q.odb(),
685                &request,
686                policy,
687                &shallow_info,
688                &receiver_grafts,
689            )?
690        }
691        _ => ShallowPlan::none(request.commands.len()),
692    };
693
694    let mut hook_records = Vec::new();
695    let updates = if unpack_error.is_some() {
696        request
697            .commands
698            .iter()
699            .map(|c| c.refused(RefUpdateRefusal::UnpackerError))
700            .collect()
701    } else {
702        stop.poll().map_err(|_| ServeError::Cancelled)?;
703        apply_commands_with_hooks(
704            repo,
705            &request,
706            opts,
707            policy,
708            hook,
709            quarantine.as_mut(),
710            &unconnected,
711            &shallow,
712            sideband.then_some(&mut *output),
713            &mut hook_records,
714        )?
715    };
716
717    if sideband {
718        if let Some(text) = hook.message(&updates) {
719            write_progress_text(output, &text)?;
720        }
721    }
722    if request.caps.contains("report-status") || request.caps.contains("report-status-v2") {
723        let report = build_report(unpack_error.as_ref(), &updates)?;
724        if sideband {
725            crate::pkt_line::write_sideband_channel1_64k(&mut &mut *output, &report)?;
726            write_flush(output)?;
727        } else {
728            output.write_all(&report)?;
729        }
730    }
731    output.flush()?;
732
733    Ok(ReceiveOutcome {
734        unpack_error,
735        updates,
736        hooks: hook_records,
737        push_options: request.push_options.clone(),
738    })
739}
740
741/// Send `text` on the progress side band (channel 2), one line per packet,
742/// as Git relays a hook script's output.
743fn write_progress_text(output: &mut dyn Write, text: &str) -> Result<()> {
744    for line in text.lines() {
745        let line = format!("{line}\n");
746        for chunk in line.as_bytes().chunks(65515) {
747            crate::pkt_line::write_sideband_packet(&mut *output, 2, chunk)
748                .map_err(ServeError::Io)?;
749        }
750    }
751    Ok(())
752}
753
754/// Write the refs the client may update, with capabilities on the first line.
755fn write_advertisement(
756    repo: &Repository,
757    output: &mut dyn Write,
758    opts: &ServeOptions,
759) -> Result<()> {
760    let mut caps: Vec<String> = CAPABILITIES.iter().map(|c| (*c).to_owned()).collect();
761    let config = repo
762        .config()
763        .unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()));
764    if config
765        .get_bool("receive.advertisepushoptions")
766        .and_then(|r| r.ok())
767        .unwrap_or(false)
768    {
769        caps.push("push-options".to_owned());
770    }
771    caps.push(format!("object-format={}", repo.odb().hash_algo().name()));
772    if let Some(path) = &opts.push_bundle {
773        caps.push(format!("{}={path}", super::PUSH_BUNDLE_CAPABILITY));
774    }
775    if !opts.agent.is_empty() {
776        caps.push(format!("agent={}", opts.agent));
777    }
778    let caps = caps.join(" ");
779
780    let mut first_line = true;
781    for_each_advertised_ref(repo, &opts.hidden_refs, &[], Peel::No, |r| {
782        if first_line {
783            write_line(output, &format!("{} {}\0{caps}", r.oid.to_hex(), r.name))?;
784            first_line = false;
785        } else {
786            write_line(output, &format!("{} {}", r.oid.to_hex(), r.name))?;
787        }
788        Ok(std::ops::ControlFlow::Continue(()))
789    })?;
790    if first_line {
791        let zero = ObjectId::null(repo.odb().hash_algo());
792        write_line(
793            output,
794            &format!("{} capabilities^{{}}\0{caps}", zero.to_hex()),
795        )?;
796    }
797    write_flush(output)
798}
799
800/// The command list of a push.
801struct PushRequest {
802    commands: Vec<RefCommand>,
803    caps: HashSet<String>,
804    push_options: Vec<String>,
805    /// The client's shallow boundaries, from `shallow <oid>` lines.
806    shallow: Vec<ObjectId>,
807}
808
809/// Read the command list up to its flush. Returns `None` when the client sent
810/// no commands (nothing to push).
811///
812/// A shallow client sends a `shallow <oid>` line for each of its boundaries
813/// before the commands; they are collected wherever they appear in the list.
814fn read_commands(repo: &Repository, input: &mut dyn Read) -> Result<Option<PushRequest>> {
815    let mut commands = Vec::new();
816    let mut caps = HashSet::new();
817    let mut shallow = Vec::new();
818    loop {
819        match read_packet(input)? {
820            None | Some(Packet::Flush) => break,
821            Some(Packet::Data(line)) => {
822                if let Some(hex) = line.strip_prefix("shallow ") {
823                    shallow.push(parse_oid(repo, hex)?);
824                    continue;
825                }
826                let (line, line_caps) = line.split_once('\0').unwrap_or((line.as_str(), ""));
827                if commands.is_empty() {
828                    caps.extend(
829                        line_caps
830                            .split(' ')
831                            .filter(|c| !c.is_empty())
832                            .map(str::to_owned),
833                    );
834                }
835                let mut parts = line.splitn(3, ' ');
836                let (Some(old), Some(new), Some(refname)) =
837                    (parts.next(), parts.next(), parts.next())
838                else {
839                    return Err(ServeError::Protocol(format!("malformed command '{line}'")));
840                };
841                let old = parse_oid(repo, old)?;
842                let new = parse_oid(repo, new)?;
843                commands.push(RefCommand {
844                    old: (!old.is_zero()).then_some(old),
845                    new: (!new.is_zero()).then_some(new),
846                    refname: refname.to_owned(),
847                });
848            }
849            Some(other) => {
850                return Err(ServeError::Protocol(format!(
851                    "unexpected packet in command list: {other:?}"
852                )))
853            }
854        }
855    }
856    if commands.is_empty() {
857        return Ok(None);
858    }
859    let mut push_options = Vec::new();
860    if caps.contains("push-options") {
861        loop {
862            match read_packet(input)? {
863                None | Some(Packet::Flush) => break,
864                Some(Packet::Data(line)) => {
865                    push_options.push(line);
866                }
867                Some(other) => {
868                    return Err(ServeError::Protocol(format!(
869                        "unexpected packet in push options: {other:?}"
870                    )))
871                }
872            }
873        }
874    }
875    let format = caps.iter().find_map(|c| c.strip_prefix("object-format="));
876    check_object_format(repo, format)?;
877    Ok(Some(PushRequest {
878        commands,
879        caps,
880        push_options,
881        shallow,
882    }))
883}
884
885/// The shallow-file changes a push from a shallow client needs.
886struct ShallowPlan {
887    /// For each command, in request order, the boundaries accepting it would
888    /// add to the repository's `shallow` file.
889    roots: Vec<Vec<ObjectId>>,
890    /// Under [`ReceivePolicy::shallow_update`], a shallow file listing the
891    /// repository's boundaries and the client's, which hooks see as
892    /// `GIT_SHALLOW_FILE` so they can walk the pushed history.
893    hook_file: Option<tempfile::NamedTempFile>,
894}
895
896impl ShallowPlan {
897    /// No shallow-file changes for `commands` commands.
898    fn none(commands: usize) -> Self {
899        Self {
900            roots: vec![Vec::new(); commands],
901            hook_file: None,
902        }
903    }
904
905    /// Work out the new boundaries of every command once the pushed objects
906    /// are in `odb`.
907    fn new(
908        repo: &Repository,
909        odb: &Odb,
910        request: &PushRequest,
911        policy: &ReceivePolicy,
912        info: &PushShallowInfo,
913        receiver_grafts: &HashSet<ObjectId>,
914    ) -> Result<Self> {
915        if info.is_empty() {
916            return Ok(Self::none(request.commands.len()));
917        }
918        let mut receiver_tips: HashSet<ObjectId> = HashSet::new();
919        super::for_each_advertised_ref(repo, &[], &[], Peel::Yes, |r| {
920            receiver_tips.insert(r.peeled.unwrap_or(r.oid));
921            Ok(std::ops::ControlFlow::Continue(()))
922        })?;
923        receiver_tips.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
924        let graph = repo.odb().commit_graph_chain();
925        let roots = request
926            .commands
927            .iter()
928            .map(|c| match c.new {
929                Some(new) => {
930                    info.new_roots(odb, graph.as_deref(), new, receiver_grafts, &receiver_tips)
931                }
932                None => Ok(Vec::new()),
933            })
934            .collect::<crate::error::Result<_>>()?;
935        let hook_file = if policy.shallow_update {
936            Some(temporary_shallow_file(
937                repo,
938                receiver_grafts,
939                &request.shallow,
940            )?)
941        } else {
942            None
943        };
944        Ok(Self { roots, hook_file })
945    }
946
947    /// `GIT_SHALLOW_FILE` for hook scripts, when set.
948    fn hook_env(&self) -> Result<Vec<(String, String)>> {
949        let Some(file) = &self.hook_file else {
950            return Ok(Vec::new());
951        };
952        let path = std::path::absolute(file.path())?;
953        Ok(vec![(
954            "GIT_SHALLOW_FILE".to_owned(),
955            path.to_string_lossy().into_owned(),
956        )])
957    }
958}
959
960/// Write the union of `receiver_grafts` and `client` to a temporary shallow
961/// file next to the repository's own.
962fn temporary_shallow_file(
963    repo: &Repository,
964    receiver_grafts: &HashSet<ObjectId>,
965    client: &[ObjectId],
966) -> Result<tempfile::NamedTempFile> {
967    let mut hexes: Vec<String> = receiver_grafts
968        .iter()
969        .chain(client)
970        .map(ObjectId::to_hex)
971        .collect();
972    hexes.sort_unstable();
973    hexes.dedup();
974    let dir = crate::shallow::shallow_file_path(repo.git_dir())
975        .parent()
976        .map(std::path::Path::to_path_buf)
977        .unwrap_or_else(|| repo.git_dir().to_path_buf());
978    let mut file = tempfile::Builder::new()
979        .prefix("shallow_")
980        .tempfile_in(dir)?;
981    for hex in hexes {
982        writeln!(file, "{hex}")?;
983    }
984    file.flush()?;
985    Ok(file)
986}
987
988/// Validate, run hooks, migrate quarantine objects, and apply ref updates.
989#[allow(clippy::too_many_arguments)]
990fn apply_commands_with_hooks(
991    repo: &Repository,
992    request: &PushRequest,
993    opts: &ServeOptions,
994    policy: &ReceivePolicy,
995    hook: &dyn ReceiveHook,
996    quarantine: Option<&mut ReceiveQuarantine>,
997    unconnected: &HashMap<ObjectId, Unconnected>,
998    shallow: &ShallowPlan,
999    mut sideband_out: Option<&mut dyn Write>,
1000    hook_records: &mut Vec<HookRunRecord>,
1001) -> Result<Vec<RefUpdateResult>> {
1002    let lookup_odb = quarantine.as_ref().map(|q| q.odb());
1003    let head_branch = ref_update_policy::head_branch(repo);
1004    let checked_out = ref_update_policy::checked_out_branch(repo);
1005    let branches = ref_update_policy::HeadBranches {
1006        head: head_branch.as_deref(),
1007        checked_out: checked_out.as_deref(),
1008    };
1009    let mut results: Vec<RefUpdateResult> = request
1010        .commands
1011        .iter()
1012        .zip(&shallow.roots)
1013        .map(|(c, roots)| {
1014            if !roots.is_empty() && !policy.shallow_update {
1015                return Ok(c.refused(RefUpdateRefusal::ShallowUpdateNotAllowed));
1016            }
1017            Ok(
1018                match ref_update_policy::validate_ref_update(
1019                    repo,
1020                    lookup_odb.as_ref(),
1021                    c,
1022                    policy,
1023                    &opts.hidden_refs,
1024                    branches,
1025                )? {
1026                    Some(reason) => c.refused(reason),
1027                    None => match c.new.and_then(|new| unconnected.get(&new)) {
1028                        Some(gap) => c.refused(RefUpdateRefusal::Unconnected {
1029                            missing: gap.oid,
1030                            referrer: gap.referrer,
1031                        }),
1032                        None => c.accepted(),
1033                    },
1034                },
1035            )
1036        })
1037        .collect::<Result<_>>()?;
1038
1039    let atomic = request.caps.contains("atomic");
1040    if atomic && results.iter().any(|r| r.error.is_some()) {
1041        refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1042        return Ok(results);
1043    }
1044
1045    let accepted: Vec<RefCommand> = request
1046        .commands
1047        .iter()
1048        .zip(&results)
1049        .filter(|(_, r)| r.error.is_none())
1050        .map(|(c, _)| c.clone())
1051        .collect();
1052    let hook_ctx = ReceiveHookContext {
1053        push_options: &request.push_options,
1054    };
1055    if !accepted.is_empty() {
1056        if let Err(refusal) = hook.pre_receive_ctx(
1057            &accepted,
1058            lookup_odb.as_ref().unwrap_or(repo.odb()),
1059            &hook_ctx,
1060        ) {
1061            refuse_remaining(&mut results, &refusal);
1062            return Ok(results);
1063        }
1064    }
1065
1066    let scripts = ReceiveHookScripts::new(repo, &request.push_options, shallow.hook_env()?);
1067    let all_cmds: Vec<&RefCommand> = request.commands.iter().collect();
1068    let pre_result = scripts.pre_receive(&all_cmds, quarantine.as_deref());
1069    relay_hook_sideband(&mut sideband_out, &pre_result)?;
1070    hook_records.push(hook_record("pre-receive", &pre_result));
1071    if !pre_result.is_ok() {
1072        refuse_remaining(&mut results, &RefUpdateRefusal::PreReceiveHookDeclined);
1073        return Ok(results);
1074    }
1075
1076    if results.iter().all(|r| r.error.is_some()) {
1077        return Ok(results);
1078    }
1079
1080    let mut migrated_pack_keeps = Vec::new();
1081    if let Some(q) = quarantine {
1082        migrated_pack_keeps = q.migrate().map_err(ServeError::Repository)?;
1083        repo.odb().invalidate_packs();
1084    }
1085
1086    for (cmd, result) in request.commands.iter().zip(results.iter_mut()) {
1087        if result.error.is_some() {
1088            continue;
1089        }
1090        if let Err(refusal) = hook.update_ctx(cmd, repo.odb(), &hook_ctx) {
1091            result.error = Some(refusal);
1092            continue;
1093        }
1094        let update_result = scripts.update(cmd);
1095        relay_hook_sideband(&mut sideband_out, &update_result)?;
1096        hook_records.push(hook_record("update", &update_result));
1097        if !update_result.is_ok() {
1098            result.error = Some(RefUpdateRefusal::UpdateHookDeclined);
1099        }
1100    }
1101
1102    if atomic && results.iter().any(|r| r.error.is_some()) {
1103        refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1104        return Ok(results);
1105    }
1106
1107    let new_roots: HashSet<ObjectId> = shallow
1108        .roots
1109        .iter()
1110        .zip(&results)
1111        .filter(|(_, r)| r.error.is_none())
1112        .flat_map(|(roots, _)| roots.iter().copied())
1113        .collect();
1114    if let Err(e) = add_shallow_boundaries(repo.git_dir(), &new_roots) {
1115        let refusal = RefUpdateRefusal::RefStore(e.to_string());
1116        for (roots, result) in shallow.roots.iter().zip(results.iter_mut()) {
1117            if !roots.is_empty() && result.error.is_none() {
1118                result.error = Some(refusal.clone());
1119            }
1120        }
1121        if atomic {
1122            refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1123            return Ok(results);
1124        }
1125    }
1126
1127    let pending: Vec<(usize, crate::refs::store::RefUpdate)> = request
1128        .commands
1129        .iter()
1130        .zip(results.iter())
1131        .enumerate()
1132        .filter(|(_, (_, r))| r.error.is_none())
1133        .map(|(idx, (cmd, _))| (idx, receive_command_store_update(cmd)))
1134        .collect();
1135
1136    let store = repo.refs();
1137    let ref_lock_wait = store.ref_lock_wait();
1138    if atomic {
1139        let batch: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1140        if let Err(e) =
1141            crate::refs::commit_ref_updates_on_store_with_wait(store, &batch, || ref_lock_wait)
1142        {
1143            let refusal = RefUpdateRefusal::RefStore(e.to_string());
1144            for (idx, _) in pending {
1145                if results[idx].error.is_none() {
1146                    results[idx].error = Some(refusal.clone());
1147                }
1148            }
1149        }
1150    } else {
1151        for (idx, message) in commit_receive_ref_updates_on_store(store, pending, ref_lock_wait) {
1152            results[idx].error = Some(RefUpdateRefusal::RefStore(message));
1153        }
1154    }
1155
1156    let successful: Vec<&RefCommand> = request
1157        .commands
1158        .iter()
1159        .zip(results.iter())
1160        .filter(|(_, r)| r.error.is_none())
1161        .map(|(c, _)| c)
1162        .collect();
1163    if !successful.is_empty() {
1164        let post_result = scripts.post_receive(&successful);
1165        relay_hook_sideband(&mut sideband_out, &post_result)?;
1166        hook_records.push(hook_record("post-receive", &post_result));
1167
1168        let pu_result = scripts.post_update(&successful);
1169        relay_hook_sideband(&mut sideband_out, &pu_result)?;
1170        hook_records.push(hook_record("post-update", &pu_result));
1171    }
1172
1173    drop(migrated_pack_keeps);
1174
1175    Ok(results)
1176}
1177
1178/// Refuse every update that has not been refused yet.
1179fn refuse_remaining(results: &mut [RefUpdateResult], reason: &RefUpdateRefusal) {
1180    for r in results.iter_mut().filter(|r| r.error.is_none()) {
1181        r.error = Some(reason.clone());
1182    }
1183}
1184
1185fn hook_record(name: &str, result: &HookResult) -> HookRunRecord {
1186    HookRunRecord {
1187        name: name.to_owned(),
1188        ran: result.was_executed(),
1189        exit_code: result.status(),
1190    }
1191}
1192
1193/// The receiving repository's hook scripts for one push, run with Git's arguments, stdin
1194/// and environment.
1195///
1196/// Every script runs from the repository directory with `GIT_DIR=.` (see
1197/// [`run_hook_in_git_dir`]) and its output captured. `pre-receive` and `post-receive` get the
1198/// push options as `GIT_PUSH_OPTION_COUNT` / `GIT_PUSH_OPTION_<n>`; `update` and
1199/// `post-update` do not, as in Git. Each `*_env` pair list also carries the extra
1200/// variables given to [`Self::new`] (such as `GIT_SHALLOW_FILE`).
1201pub(crate) struct ReceiveHookScripts<'a> {
1202    repo: &'a Repository,
1203    /// Push options plus the extra variables, for `pre-receive` and `post-receive`.
1204    receive_env: Vec<(String, String)>,
1205    /// The extra variables alone, for `update` and `post-update`.
1206    update_env: Vec<(String, String)>,
1207}
1208
1209impl<'a> ReceiveHookScripts<'a> {
1210    /// Scripts of `repo` for a push that sent `push_options`, with `extra_env` set for every
1211    /// script.
1212    pub(crate) fn new(
1213        repo: &'a Repository,
1214        push_options: &[String],
1215        extra_env: Vec<(String, String)>,
1216    ) -> Self {
1217        let mut receive_env = Vec::with_capacity(push_options.len() + 1 + extra_env.len());
1218        receive_env.push((
1219            "GIT_PUSH_OPTION_COUNT".to_owned(),
1220            push_options.len().to_string(),
1221        ));
1222        receive_env.extend(
1223            push_options
1224                .iter()
1225                .enumerate()
1226                .map(|(i, opt)| (format!("GIT_PUSH_OPTION_{i}"), opt.clone())),
1227        );
1228        receive_env.extend(extra_env.iter().cloned());
1229        Self {
1230            repo,
1231            receive_env,
1232            update_env: extra_env,
1233        }
1234    }
1235
1236    /// Run `pre-receive` over `commands` (one `<old> <new> <ref>` line each on stdin).
1237    ///
1238    /// With `quarantine`, the script also gets its variables so it can read the pushed
1239    /// objects before they enter the repository.
1240    pub(crate) fn pre_receive(
1241        &self,
1242        commands: &[&RefCommand],
1243        quarantine: Option<&ReceiveQuarantine>,
1244    ) -> HookResult {
1245        let mut env = self.receive_env.clone();
1246        if let Some(q) = quarantine {
1247            env.extend(q.hook_env());
1248        }
1249        self.run_with_commands("pre-receive", commands, &env)
1250    }
1251
1252    /// Run `update` for one command, with `<ref> <old> <new>` as arguments.
1253    pub(crate) fn update(&self, cmd: &RefCommand) -> HookResult {
1254        let algo = self.repo.odb().hash_algo();
1255        let hex = |oid: Option<ObjectId>| oid.unwrap_or_else(|| ObjectId::null(algo)).to_hex();
1256        let (old, new) = (hex(cmd.old), hex(cmd.new));
1257        let args = [cmd.refname.as_str(), old.as_str(), new.as_str()];
1258        run_hook_in_git_dir(self.repo, "update", &args, None, &pairs(&self.update_env))
1259    }
1260
1261    /// Run `post-receive` over the applied `commands`.
1262    pub(crate) fn post_receive(&self, commands: &[&RefCommand]) -> HookResult {
1263        self.run_with_commands("post-receive", commands, &self.receive_env)
1264    }
1265
1266    /// Run `post-update` with the names of the applied `commands` as arguments.
1267    pub(crate) fn post_update(&self, commands: &[&RefCommand]) -> HookResult {
1268        let args: Vec<&str> = commands.iter().map(|c| c.refname.as_str()).collect();
1269        run_hook_in_git_dir(
1270            self.repo,
1271            "post-update",
1272            &args,
1273            None,
1274            &pairs(&self.update_env),
1275        )
1276    }
1277
1278    fn run_with_commands(
1279        &self,
1280        hook_name: &str,
1281        commands: &[&RefCommand],
1282        env: &[(String, String)],
1283    ) -> HookResult {
1284        let zero = ObjectId::null(self.repo.odb().hash_algo());
1285        let stdin: String = commands
1286            .iter()
1287            .map(|c| {
1288                format!(
1289                    "{} {} {}\n",
1290                    c.old.unwrap_or(zero).to_hex(),
1291                    c.new.unwrap_or(zero).to_hex(),
1292                    c.refname
1293                )
1294            })
1295            .collect();
1296        run_hook_in_git_dir(
1297            self.repo,
1298            hook_name,
1299            &[],
1300            Some(stdin.as_bytes()),
1301            &pairs(env),
1302        )
1303    }
1304}
1305
1306fn pairs(env: &[(String, String)]) -> Vec<(&str, &str)> {
1307    env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
1308}
1309
1310fn receive_updates_eligible_for_initial_packed(updates: &[crate::refs::store::RefUpdate]) -> bool {
1311    use crate::refs::store::{Expected, RawRef};
1312    updates.iter().all(|update| {
1313        matches!(update.new_value, Some(RawRef::Direct(_)))
1314            && matches!(update.expected, Expected::Missing)
1315            && !update.flags.log_only
1316            && update.reflog.is_none()
1317    })
1318}
1319
1320/// Apply a non-atomic receive-pack ref batch in one store transaction when possible.
1321///
1322/// Plain creates use the initial packed-refs path; mixed batches use a regular transaction.
1323/// When a batch fails, fall back to a non-atomic batch (one reftable table write per phase)
1324/// so sibling updates still commit while individual refs can be rejected.
1325fn commit_receive_ref_updates_on_store(
1326    store: &dyn crate::refs::store::RefStore,
1327    pending: Vec<(usize, crate::refs::store::RefUpdate)>,
1328    ref_lock_wait: crate::lockfile::LockWait,
1329) -> Vec<(usize, String)> {
1330    if pending.is_empty() {
1331        return Vec::new();
1332    }
1333    let updates: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1334    let batch_ok = if receive_updates_eligible_for_initial_packed(&updates) {
1335        crate::refs::commit_initial_ref_updates_on_store_with_wait(store, updates.clone(), || {
1336            ref_lock_wait
1337        })
1338    } else {
1339        crate::refs::commit_ref_updates_on_store_with_wait(store, &updates, || ref_lock_wait)
1340    };
1341    if batch_ok.is_ok() {
1342        return Vec::new();
1343    }
1344    let indices: Vec<_> = pending.iter().map(|(idx, _)| *idx).collect();
1345    match crate::refs::commit_ref_updates_non_atomic_on_store(store, updates) {
1346        Ok(outcomes) => indices
1347            .into_iter()
1348            .zip(outcomes)
1349            .filter_map(|(idx, outcome)| outcome.result.err().map(|e| (idx, e.to_string())))
1350            .collect(),
1351        Err(e) => indices
1352            .into_iter()
1353            .map(|idx| (idx, e.to_string()))
1354            .collect(),
1355    }
1356}
1357
1358fn receive_command_store_update(cmd: &RefCommand) -> crate::refs::store::RefUpdate {
1359    let mut flags = crate::refs::store::RefUpdateFlags::default();
1360    if cmd.refname == "HEAD" {
1361        flags.no_deref = true;
1362    }
1363    crate::refs::store::RefUpdate {
1364        name: cmd.refname.clone(),
1365        new_value: cmd.new.map(crate::refs::store::RawRef::Direct),
1366        expected: match cmd.old {
1367            Some(oid) => crate::refs::store::Expected::Oid(oid),
1368            None => crate::refs::store::Expected::Missing,
1369        },
1370        reflog: None,
1371        flags,
1372    }
1373}
1374
1375/// Relay what a hook wrote (stdout, then stderr) to the client on sideband 2.
1376fn relay_hook_sideband(out: &mut Option<&mut dyn Write>, result: &HookResult) -> Result<()> {
1377    let (Some(w), Some(output)) = (out.as_mut(), result.output()) else {
1378        return Ok(());
1379    };
1380    for chunk in output
1381        .stdout
1382        .chunks(65515)
1383        .chain(output.stderr.chunks(65515))
1384    {
1385        crate::pkt_line::write_sideband_packet(*w, 2, chunk).map_err(ServeError::Io)?;
1386    }
1387    Ok(())
1388}
1389
1390/// Encode the status report as pkt-lines: the unpack result, one line per ref,
1391/// and a closing flush.
1392fn build_report(
1393    unpack_error: Option<&UnpackError>,
1394    updates: &[RefUpdateResult],
1395) -> Result<Vec<u8>> {
1396    let mut report = Vec::new();
1397    match unpack_error {
1398        None => write_line(&mut report, "unpack ok")?,
1399        Some(e) => write_line(&mut report, &format!("unpack {}", one_line(&e.to_string())))?,
1400    }
1401    for u in updates {
1402        match &u.error {
1403            None => write_line(&mut report, &format!("ok {}", u.refname))?,
1404            Some(reason) => write_line(
1405                &mut report,
1406                &format!("ng {} {}", u.refname, one_line(&reason.to_string())),
1407            )?,
1408        }
1409    }
1410    write_flush(&mut report)?;
1411    Ok(report)
1412}
1413
1414/// Collapse a message to a single line so it fits in one status pkt-line.
1415fn one_line(msg: &str) -> String {
1416    msg.lines().next().unwrap_or_default().trim().to_owned()
1417}
1418
1419/// A pushed pack written to a temp file in the quarantine's `pack/` directory.
1420struct SpooledPack {
1421    file: tempfile::NamedTempFile,
1422    /// The 12-byte pack header (signature, version, object count).
1423    header: [u8; 12],
1424}
1425
1426/// Stream exactly one packfile from the push stream into a temp file.
1427///
1428/// The pack's end is found by scanning its entries as they arrive, and nothing
1429/// past the trailing checksum is requested: the push client keeps its write
1430/// half open until it has read the status report, so waiting for end of input
1431/// would hang (issue #1656). The checksum itself is verified when the pack is
1432/// stored.
1433fn spool_push_pack(
1434    input: &mut dyn Read,
1435    odb: &Odb,
1436    max_bytes: Option<u64>,
1437    stop: &CancelSignal<'_>,
1438) -> std::result::Result<SpooledPack, UnpackError> {
1439    let hash = odb.hash_algo();
1440    let mut file = tempfile::Builder::new()
1441        .prefix("tmp_pack_")
1442        .tempfile_in(odb.objects_dir().join("pack"))?;
1443    let mut scanner = PackStreamScanner::new(hash);
1444    let mut header = [0u8; 12];
1445    let mut chunk = vec![0u8; SPOOL_CHUNK];
1446    let mut written = 0u64;
1447    let mut pack_len: Option<u64> = None;
1448    let mut out = BufWriter::with_capacity(SPOOL_CHUNK, file.as_file_mut());
1449    loop {
1450        stop.poll().map_err(|_| UnpackError::Cancelled)?;
1451        let want = match pack_len {
1452            Some(len) if written >= len => break,
1453            Some(len) => usize::try_from(len - written).map_or(chunk.len(), |n| n.min(chunk.len())),
1454            None => chunk.len(),
1455        };
1456        let n = read_some(input, &mut chunk[..want])?;
1457        if n == 0 {
1458            return Err(UnpackError::Truncated);
1459        }
1460        let mut data = &chunk[..n];
1461        if pack_len.is_none() {
1462            scanner.feed(data)?;
1463            if scanner.is_complete() {
1464                let len = scanner
1465                    .pack_body_byte_len()
1466                    .saturating_add(hash.len() as u64);
1467                let keep = usize::try_from(len.saturating_sub(written)).map_or(n, |k| k.min(n));
1468                data = &data[..keep];
1469                pack_len = Some(len);
1470            }
1471        }
1472        if let Some(limit) = max_bytes {
1473            let total = pack_len.unwrap_or(written.saturating_add(data.len() as u64));
1474            if total > limit {
1475                return Err(UnpackError::TooLarge { limit });
1476            }
1477        }
1478        if let Some(missing) = usize::try_from(written)
1479            .ok()
1480            .and_then(|start| header.get_mut(start..))
1481        {
1482            let take = missing.len().min(data.len());
1483            missing[..take].copy_from_slice(&data[..take]);
1484        }
1485        out.write_all(data)?;
1486        written += data.len() as u64;
1487    }
1488    out.flush()?;
1489    drop(out);
1490    Ok(SpooledPack { file, header })
1491}
1492
1493/// `Read::read`, retrying when interrupted by a signal.
1494fn read_some(input: &mut dyn Read, buf: &mut [u8]) -> std::io::Result<usize> {
1495    loop {
1496        match input.read(buf) {
1497            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1498            other => return other,
1499        }
1500    }
1501}
1502
1503/// Check the objects a push stored in the quarantine `odb` against `fsck`.
1504///
1505/// `indexed` lists the objects of an installed pack; `None` means the pack was
1506/// exploded into loose objects, which are then all the quarantine holds.
1507fn check_received_objects(
1508    repo: &Repository,
1509    odb: &Odb,
1510    indexed: Option<&HashSet<ObjectId>>,
1511    cfg: &ConfigSet,
1512    fsck: &ObjectFsckPolicy,
1513) -> std::result::Result<(), UnpackError> {
1514    let checker = fsck.checker(odb.hash_algo(), repo.git_dir(), repo.environment())?;
1515    let mut received: Vec<ObjectId> = match indexed {
1516        Some(ids) => ids.iter().copied().collect(),
1517        None => odb
1518            .enumerate_local_loose_objects()?
1519            .into_iter()
1520            .map(|(oid, _)| oid)
1521            .collect(),
1522    };
1523    received.sort_unstable();
1524    let parallel = repo
1525        .thread_policy()
1526        .try_resolve(ParallelOp::IndexPack, cfg)
1527        .map_err(crate::error::Error::from)?;
1528    let verdicts = try_par_map(&received, parallel, received.len(), |oid| {
1529        if odb.read_info(oid)?.kind == ObjectKind::Blob {
1530            return Ok(None);
1531        }
1532        let obj = odb.read(oid)?;
1533        Ok(checker
1534            .check(oid, obj.kind, &obj.data)
1535            .err()
1536            .map(|error| RejectedObject {
1537                oid: *oid,
1538                kind: obj.kind,
1539                error,
1540            }))
1541    })
1542    .map_err(|ParallelHashError::Task(e): ParallelHashError<crate::error::Error>| e)?;
1543    match verdicts.into_iter().flatten().next() {
1544        Some(rejected) => Err(UnpackError::Rejected(rejected)),
1545        None => Ok(()),
1546    }
1547}
1548
1549/// The new tips among `commands` that reach an object that is in neither the push
1550/// (`received`, read through the quarantine `odb`) nor `repo`, unless a promisor
1551/// remote promises it, with the first such object. The walk stops at what
1552/// `existing` (the values of `repo`'s refs and `HEAD` before the push) reaches
1553/// (see [`super::connectivity`]).
1554fn unconnected_tips(
1555    repo: &Repository,
1556    odb: &Odb,
1557    received: &HashSet<ObjectId>,
1558    existing: &HashSet<ObjectId>,
1559    commands: &[RefCommand],
1560    shallow_boundaries: &HashSet<ObjectId>,
1561    promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1562) -> std::result::Result<HashMap<ObjectId, Unconnected>, UnpackError> {
1563    let mut tips: Vec<ObjectId> = commands.iter().filter_map(|c| c.new).collect();
1564    tips.sort_unstable();
1565    tips.dedup();
1566    if tips.is_empty() {
1567        return Ok(HashMap::new());
1568    }
1569    let receiver_grafts = load_shallow_boundaries(repo.git_dir());
1570    let graph = repo.odb().commit_graph_chain();
1571    let pushed = PushedObjects {
1572        odb,
1573        repo_odb: repo.odb(),
1574        graph: graph.as_deref(),
1575        received,
1576        existing,
1577        shallow_boundaries,
1578        receiver_grafts: &receiver_grafts,
1579        promises,
1580    };
1581    let gaps = pushed.unconnected_tips(&tips)?;
1582    Ok(tips
1583        .into_iter()
1584        .zip(gaps)
1585        .filter_map(|(tip, gap)| Some((tip, gap?)))
1586        .collect())
1587}
1588
1589/// The values of every ref of `repo` and of `HEAD`, with tags also peeled.
1590fn existing_ref_values(repo: &Repository) -> Result<HashSet<ObjectId>> {
1591    let mut existing = super::advertised_object_ids_from_repo(repo, &[], &[], super::Peel::Yes)?;
1592    existing.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
1593    Ok(existing)
1594}
1595
1596/// Store a spooled push pack: explode small packs into loose objects, index and
1597/// install larger ones, per `receive.unpacklimit`.
1598///
1599/// Returns the ids of the installed pack's objects; `None` when the objects
1600/// were unpacked as loose files.
1601///
1602/// The loose path checks that every pushed object's references resolve,
1603/// except the parents of `shallow_boundaries` (the pushing client is shallow
1604/// there and does not have them) and, with `promises`, objects a promisor
1605/// remote of the repository has: Git's connectivity check skips those too, so a
1606/// push to a repository whose large blobs live on a promisor works.
1607fn ingest_spooled_pack(
1608    odb: &Odb,
1609    pack: SpooledPack,
1610    cfg: &ConfigSet,
1611    shallow_boundaries: &HashSet<ObjectId>,
1612    promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1613) -> std::result::Result<Option<HashSet<ObjectId>>, UnpackError> {
1614    if should_use_unpack_objects(&pack.header, cfg) {
1615        let mut reader = BufReader::with_capacity(SPOOL_CHUNK, pack.file.reopen()?);
1616        let opts = UnpackOptions {
1617            quiet: true,
1618            strict: true,
1619            shallow_boundaries: shallow_boundaries.clone(),
1620            ..UnpackOptions::default()
1621        };
1622        match promises {
1623            Some(promises) => {
1624                let promised =
1625                    |oid: &ObjectId, referrer: &ObjectId| promises.is_promised(oid, Some(referrer));
1626                crate::unpack_objects::unpack_objects_with_promises(
1627                    &mut reader,
1628                    odb,
1629                    &opts,
1630                    &promised,
1631                )?;
1632            }
1633            None => {
1634                unpack_objects(&mut reader, odb, &opts)?;
1635            }
1636        }
1637        return Ok(None);
1638    }
1639    let path = pack.file.into_temp_path();
1640    let installed = install_pack_path(
1641        &path,
1642        odb,
1643        &IngestPackOptions {
1644            fix_thin: true,
1645            // Indexing hashes every object and the trailer is checked while
1646            // installing, so a second full read of the pack proves nothing.
1647            skip_post_index_verify: true,
1648            merged_config: Some(cfg.clone()),
1649            ..Default::default()
1650        },
1651    )?;
1652    Ok(Some(installed.object_ids))
1653}
1654
1655#[cfg(test)]
1656mod tests {
1657    use super::*;
1658
1659    #[test]
1660    fn report_lists_unpack_status_then_each_ref() {
1661        let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
1662            .unwrap_or_else(|_| ObjectId::zero());
1663        let updates = vec![
1664            RefUpdateResult {
1665                refname: "refs/heads/main".into(),
1666                old: None,
1667                new: Some(oid),
1668                error: None,
1669            },
1670            RefUpdateResult {
1671                refname: "refs/heads/topic".into(),
1672                old: Some(oid),
1673                new: None,
1674                error: Some(RefUpdateRefusal::DeletionProhibited),
1675            },
1676        ];
1677        let report = build_report(None, &updates).unwrap_or_default();
1678        assert_eq!(
1679            String::from_utf8_lossy(&report),
1680            "000eunpack ok\n0017ok refs/heads/main\n002cng refs/heads/topic deletion prohibited\n0000"
1681        );
1682    }
1683
1684    #[test]
1685    fn report_renders_typed_unpack_error_and_refusal() {
1686        let updates = vec![RefUpdateResult {
1687            refname: "refs/heads/main".into(),
1688            old: None,
1689            new: None,
1690            error: Some(RefUpdateRefusal::UnpackerError),
1691        }];
1692        let report = build_report(Some(&UnpackError::Truncated), &updates).unwrap_or_default();
1693        assert_eq!(
1694            String::from_utf8_lossy(&report),
1695            "0029unpack unexpected end of pack stream\n0026ng refs/heads/main unpacker error\n0000"
1696        );
1697    }
1698
1699    #[test]
1700    fn refusals_render_the_reasons_git_clients_expect() {
1701        let cases = [
1702            (RefUpdateRefusal::InvalidRefName, "funny refname"),
1703            (RefUpdateRefusal::HiddenRef, "deny updating a hidden ref"),
1704            (RefUpdateRefusal::StaleInfo, "stale info"),
1705            (RefUpdateRefusal::DeletionProhibited, "deletion prohibited"),
1706            (
1707                RefUpdateRefusal::CurrentBranchDeletion,
1708                "deletion of the current branch prohibited",
1709            ),
1710            (
1711                RefUpdateRefusal::CurrentBranch,
1712                "branch is currently checked out",
1713            ),
1714            (
1715                RefUpdateRefusal::MissingObjects,
1716                "missing necessary objects",
1717            ),
1718            (
1719                RefUpdateRefusal::Unconnected {
1720                    missing: ObjectId::zero(),
1721                    referrer: ObjectId::zero(),
1722                },
1723                "missing necessary objects",
1724            ),
1725            (RefUpdateRefusal::NonFastForward, "non-fast-forward"),
1726            (RefUpdateRefusal::BadRef, "bad ref"),
1727            (
1728                RefUpdateRefusal::FastForwardCheckFailed,
1729                "unable to verify fast-forward",
1730            ),
1731            (
1732                RefUpdateRefusal::ShallowUpdateNotAllowed,
1733                "shallow update not allowed",
1734            ),
1735            (RefUpdateRefusal::UnpackerError, "unpacker error"),
1736            (
1737                RefUpdateRefusal::AtomicTransactionFailed,
1738                "atomic transaction failed",
1739            ),
1740            (
1741                RefUpdateRefusal::PreReceiveHookDeclined,
1742                "pre-receive hook declined",
1743            ),
1744            (RefUpdateRefusal::UpdateHookDeclined, "hook declined"),
1745            (RefUpdateRefusal::Policy("frozen".into()), "frozen"),
1746            (
1747                RefUpdateRefusal::TargetBranch {
1748                    target: "main".into(),
1749                },
1750                "main is the target branch; it changes only through integrate",
1751            ),
1752            (
1753                RefUpdateRefusal::RefStore("locked".into()),
1754                "failed to update ref: locked",
1755            ),
1756        ];
1757        for (refusal, wire) in cases {
1758            assert_eq!(refusal.to_string(), wire);
1759        }
1760    }
1761
1762    /// Serves `data`, then fails any further read: reading past the pack would
1763    /// block forever on a real push connection.
1764    struct NoReadPastEnd<'a> {
1765        data: &'a [u8],
1766    }
1767
1768    impl Read for NoReadPastEnd<'_> {
1769        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1770            if self.data.is_empty() {
1771                return Err(std::io::Error::other("read past the end of the pack"));
1772            }
1773            let n = buf.len().min(self.data.len());
1774            buf[..n].copy_from_slice(&self.data[..n]);
1775            self.data = &self.data[n..];
1776            Ok(n)
1777        }
1778    }
1779
1780    fn sample_pack() -> Vec<u8> {
1781        use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
1782        let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
1783        let mut seed = 0x2545_f491_u32;
1784        // Incompressible, so the pack spans several spool chunks.
1785        let big: Vec<u8> = (0..(SPOOL_CHUNK * 3))
1786            .map(|_| {
1787                seed ^= seed << 13;
1788                seed ^= seed >> 17;
1789                seed ^= seed << 5;
1790                seed as u8
1791            })
1792            .collect();
1793        pb.add_full(PK::Blob, &big);
1794        pb.add_full(PK::Blob, b"small\n");
1795        pb.build().bytes
1796    }
1797
1798    fn test_odb(dir: &std::path::Path) -> Odb {
1799        let objects = dir.join("objects");
1800        std::fs::create_dir_all(objects.join("pack")).unwrap_or_default();
1801        Odb::new(&objects)
1802    }
1803
1804    #[test]
1805    fn spool_stops_at_the_trailer_without_reading_further() {
1806        let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1807        let odb = test_odb(tmp.path());
1808        let pack = sample_pack();
1809        let mut input = NoReadPastEnd { data: &pack };
1810        let spooled = spool_push_pack(&mut input, &odb, None, &CancelSignal::never())
1811            .unwrap_or_else(|e| panic!("{e}"));
1812        assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1813        assert_eq!(&spooled.header[..], &pack[..12]);
1814    }
1815
1816    #[test]
1817    fn spool_keeps_only_the_pack_when_more_bytes_follow() {
1818        let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1819        let odb = test_odb(tmp.path());
1820        let pack = sample_pack();
1821        let mut stream = pack.clone();
1822        stream.extend_from_slice(b"trailing junk");
1823        let spooled = spool_push_pack(&mut stream.as_slice(), &odb, None, &CancelSignal::never())
1824            .unwrap_or_else(|e| panic!("{e}"));
1825        assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1826    }
1827
1828    #[test]
1829    fn spool_reports_typed_errors() {
1830        let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1831        let odb = test_odb(tmp.path());
1832        let pack = sample_pack();
1833
1834        let truncated = &pack[..pack.len() - 5];
1835        let never = CancelSignal::never();
1836        assert!(matches!(
1837            spool_push_pack(&mut &truncated[..], &odb, None, &never),
1838            Err(UnpackError::Truncated)
1839        ));
1840
1841        let limit = (pack.len() - 1) as u64;
1842        assert!(matches!(
1843            spool_push_pack(&mut pack.as_slice(), &odb, Some(limit), &never),
1844            Err(UnpackError::TooLarge { limit: l }) if l == limit
1845        ));
1846        assert!(
1847            spool_push_pack(&mut pack.as_slice(), &odb, Some(pack.len() as u64), &never).is_ok()
1848        );
1849
1850        assert!(matches!(
1851            spool_push_pack(&mut &b"JUNK\0\0\0\x02\0\0\0\x01"[..], &odb, None, &never),
1852            Err(UnpackError::Malformed(PackStreamScanError::BadSignature))
1853        ));
1854
1855        let leftovers: Vec<_> = std::fs::read_dir(odb.objects_dir().join("pack"))
1856            .map(|rd| rd.filter_map(|e| e.ok()).collect())
1857            .unwrap_or_default();
1858        assert!(
1859            leftovers.is_empty(),
1860            "failed spools must not leave temp files"
1861        );
1862    }
1863
1864    #[test]
1865    fn deny_current_branch_values() {
1866        let policy_for = |value: Option<&str>| {
1867            let mut config = ConfigSet::new();
1868            if let Some(v) = value {
1869                let _ = config.add_command_override("receive.denyCurrentBranch", v);
1870            }
1871            ReceivePolicy::from_config(&config).deny_current_branch
1872        };
1873        assert!(policy_for(None));
1874        assert!(!policy_for(Some("ignore")));
1875        assert!(!policy_for(Some("updateInstead")));
1876        assert!(policy_for(Some("refuse")));
1877    }
1878
1879    #[test]
1880    fn shallow_update_is_off_unless_configured() {
1881        assert!(!ReceivePolicy::default().shallow_update);
1882        let mut config = ConfigSet::new();
1883        let _ = config.add_command_override("receive.shallowUpdate", "true");
1884        assert!(ReceivePolicy::from_config(&config).shallow_update);
1885    }
1886
1887    #[test]
1888    fn deny_delete_current_values() {
1889        let policy_for = |value: Option<&str>| {
1890            let mut config = ConfigSet::new();
1891            if let Some(v) = value {
1892                let _ = config.add_command_override("receive.denyDeleteCurrent", v);
1893            }
1894            ReceivePolicy::from_config(&config).deny_delete_current
1895        };
1896        assert!(policy_for(None));
1897        assert!(!policy_for(Some("ignore")));
1898        assert!(!policy_for(Some("warn")));
1899        assert!(!policy_for(Some("false")));
1900        // There is no working tree to update for a deleted branch.
1901        assert!(policy_for(Some("updateInstead")));
1902        assert!(policy_for(Some("refuse")));
1903        assert!(ReceivePolicy::default().deny_delete_current);
1904    }
1905}