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    advertised_refs, check_object_format, parse_oid, read_packet, ref_update_policy, write_flush,
10    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 refs = advertised_refs(repo, &opts.hidden_refs, &[], Peel::No)?;
781    if refs.is_empty() {
782        let zero = ObjectId::null(repo.odb().hash_algo());
783        write_line(
784            output,
785            &format!("{} capabilities^{{}}\0{caps}", zero.to_hex()),
786        )?;
787    }
788    for (i, r) in refs.iter().enumerate() {
789        if i == 0 {
790            write_line(output, &format!("{} {}\0{caps}", r.oid.to_hex(), r.name))?;
791        } else {
792            write_line(output, &format!("{} {}", r.oid.to_hex(), r.name))?;
793        }
794    }
795    write_flush(output)
796}
797
798/// The command list of a push.
799struct PushRequest {
800    commands: Vec<RefCommand>,
801    caps: HashSet<String>,
802    push_options: Vec<String>,
803    /// The client's shallow boundaries, from `shallow <oid>` lines.
804    shallow: Vec<ObjectId>,
805}
806
807/// Read the command list up to its flush. Returns `None` when the client sent
808/// no commands (nothing to push).
809///
810/// A shallow client sends a `shallow <oid>` line for each of its boundaries
811/// before the commands; they are collected wherever they appear in the list.
812fn read_commands(repo: &Repository, input: &mut dyn Read) -> Result<Option<PushRequest>> {
813    let mut commands = Vec::new();
814    let mut caps = HashSet::new();
815    let mut shallow = Vec::new();
816    loop {
817        match read_packet(input)? {
818            None | Some(Packet::Flush) => break,
819            Some(Packet::Data(line)) => {
820                if let Some(hex) = line.strip_prefix("shallow ") {
821                    shallow.push(parse_oid(repo, hex)?);
822                    continue;
823                }
824                let (line, line_caps) = line.split_once('\0').unwrap_or((line.as_str(), ""));
825                if commands.is_empty() {
826                    caps.extend(
827                        line_caps
828                            .split(' ')
829                            .filter(|c| !c.is_empty())
830                            .map(str::to_owned),
831                    );
832                }
833                let mut parts = line.splitn(3, ' ');
834                let (Some(old), Some(new), Some(refname)) =
835                    (parts.next(), parts.next(), parts.next())
836                else {
837                    return Err(ServeError::Protocol(format!("malformed command '{line}'")));
838                };
839                let old = parse_oid(repo, old)?;
840                let new = parse_oid(repo, new)?;
841                commands.push(RefCommand {
842                    old: (!old.is_zero()).then_some(old),
843                    new: (!new.is_zero()).then_some(new),
844                    refname: refname.to_owned(),
845                });
846            }
847            Some(other) => {
848                return Err(ServeError::Protocol(format!(
849                    "unexpected packet in command list: {other:?}"
850                )))
851            }
852        }
853    }
854    if commands.is_empty() {
855        return Ok(None);
856    }
857    let mut push_options = Vec::new();
858    if caps.contains("push-options") {
859        loop {
860            match read_packet(input)? {
861                None | Some(Packet::Flush) => break,
862                Some(Packet::Data(line)) => {
863                    push_options.push(line);
864                }
865                Some(other) => {
866                    return Err(ServeError::Protocol(format!(
867                        "unexpected packet in push options: {other:?}"
868                    )))
869                }
870            }
871        }
872    }
873    let format = caps.iter().find_map(|c| c.strip_prefix("object-format="));
874    check_object_format(repo, format)?;
875    Ok(Some(PushRequest {
876        commands,
877        caps,
878        push_options,
879        shallow,
880    }))
881}
882
883/// The shallow-file changes a push from a shallow client needs.
884struct ShallowPlan {
885    /// For each command, in request order, the boundaries accepting it would
886    /// add to the repository's `shallow` file.
887    roots: Vec<Vec<ObjectId>>,
888    /// Under [`ReceivePolicy::shallow_update`], a shallow file listing the
889    /// repository's boundaries and the client's, which hooks see as
890    /// `GIT_SHALLOW_FILE` so they can walk the pushed history.
891    hook_file: Option<tempfile::NamedTempFile>,
892}
893
894impl ShallowPlan {
895    /// No shallow-file changes for `commands` commands.
896    fn none(commands: usize) -> Self {
897        Self {
898            roots: vec![Vec::new(); commands],
899            hook_file: None,
900        }
901    }
902
903    /// Work out the new boundaries of every command once the pushed objects
904    /// are in `odb`.
905    fn new(
906        repo: &Repository,
907        odb: &Odb,
908        request: &PushRequest,
909        policy: &ReceivePolicy,
910        info: &PushShallowInfo,
911        receiver_grafts: &HashSet<ObjectId>,
912    ) -> Result<Self> {
913        if info.is_empty() {
914            return Ok(Self::none(request.commands.len()));
915        }
916        let mut receiver_tips: HashSet<ObjectId> = advertised_refs(repo, &[], &[], Peel::Yes)?
917            .into_iter()
918            .map(|r| r.peeled.unwrap_or(r.oid))
919            .collect();
920        receiver_tips.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
921        let graph = repo.odb().commit_graph_chain();
922        let roots = request
923            .commands
924            .iter()
925            .map(|c| match c.new {
926                Some(new) => {
927                    info.new_roots(odb, graph.as_deref(), new, receiver_grafts, &receiver_tips)
928                }
929                None => Ok(Vec::new()),
930            })
931            .collect::<crate::error::Result<_>>()?;
932        let hook_file = if policy.shallow_update {
933            Some(temporary_shallow_file(
934                repo,
935                receiver_grafts,
936                &request.shallow,
937            )?)
938        } else {
939            None
940        };
941        Ok(Self { roots, hook_file })
942    }
943
944    /// `GIT_SHALLOW_FILE` for hook scripts, when set.
945    fn hook_env(&self) -> Result<Vec<(String, String)>> {
946        let Some(file) = &self.hook_file else {
947            return Ok(Vec::new());
948        };
949        let path = std::path::absolute(file.path())?;
950        Ok(vec![(
951            "GIT_SHALLOW_FILE".to_owned(),
952            path.to_string_lossy().into_owned(),
953        )])
954    }
955}
956
957/// Write the union of `receiver_grafts` and `client` to a temporary shallow
958/// file next to the repository's own.
959fn temporary_shallow_file(
960    repo: &Repository,
961    receiver_grafts: &HashSet<ObjectId>,
962    client: &[ObjectId],
963) -> Result<tempfile::NamedTempFile> {
964    let mut hexes: Vec<String> = receiver_grafts
965        .iter()
966        .chain(client)
967        .map(ObjectId::to_hex)
968        .collect();
969    hexes.sort_unstable();
970    hexes.dedup();
971    let dir = crate::shallow::shallow_file_path(repo.git_dir())
972        .parent()
973        .map(std::path::Path::to_path_buf)
974        .unwrap_or_else(|| repo.git_dir().to_path_buf());
975    let mut file = tempfile::Builder::new()
976        .prefix("shallow_")
977        .tempfile_in(dir)?;
978    for hex in hexes {
979        writeln!(file, "{hex}")?;
980    }
981    file.flush()?;
982    Ok(file)
983}
984
985/// Validate, run hooks, migrate quarantine objects, and apply ref updates.
986#[allow(clippy::too_many_arguments)]
987fn apply_commands_with_hooks(
988    repo: &Repository,
989    request: &PushRequest,
990    opts: &ServeOptions,
991    policy: &ReceivePolicy,
992    hook: &dyn ReceiveHook,
993    quarantine: Option<&mut ReceiveQuarantine>,
994    unconnected: &HashMap<ObjectId, Unconnected>,
995    shallow: &ShallowPlan,
996    mut sideband_out: Option<&mut dyn Write>,
997    hook_records: &mut Vec<HookRunRecord>,
998) -> Result<Vec<RefUpdateResult>> {
999    let lookup_odb = quarantine.as_ref().map(|q| q.odb());
1000    let head_branch = ref_update_policy::head_branch(repo);
1001    let checked_out = ref_update_policy::checked_out_branch(repo);
1002    let branches = ref_update_policy::HeadBranches {
1003        head: head_branch.as_deref(),
1004        checked_out: checked_out.as_deref(),
1005    };
1006    let mut results: Vec<RefUpdateResult> = request
1007        .commands
1008        .iter()
1009        .zip(&shallow.roots)
1010        .map(|(c, roots)| {
1011            if !roots.is_empty() && !policy.shallow_update {
1012                return Ok(c.refused(RefUpdateRefusal::ShallowUpdateNotAllowed));
1013            }
1014            Ok(
1015                match ref_update_policy::validate_ref_update(
1016                    repo,
1017                    lookup_odb.as_ref(),
1018                    c,
1019                    policy,
1020                    &opts.hidden_refs,
1021                    branches,
1022                )? {
1023                    Some(reason) => c.refused(reason),
1024                    None => match c.new.and_then(|new| unconnected.get(&new)) {
1025                        Some(gap) => c.refused(RefUpdateRefusal::Unconnected {
1026                            missing: gap.oid,
1027                            referrer: gap.referrer,
1028                        }),
1029                        None => c.accepted(),
1030                    },
1031                },
1032            )
1033        })
1034        .collect::<Result<_>>()?;
1035
1036    let atomic = request.caps.contains("atomic");
1037    if atomic && results.iter().any(|r| r.error.is_some()) {
1038        refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1039        return Ok(results);
1040    }
1041
1042    let accepted: Vec<RefCommand> = request
1043        .commands
1044        .iter()
1045        .zip(&results)
1046        .filter(|(_, r)| r.error.is_none())
1047        .map(|(c, _)| c.clone())
1048        .collect();
1049    let hook_ctx = ReceiveHookContext {
1050        push_options: &request.push_options,
1051    };
1052    if !accepted.is_empty() {
1053        if let Err(refusal) = hook.pre_receive_ctx(
1054            &accepted,
1055            lookup_odb.as_ref().unwrap_or(repo.odb()),
1056            &hook_ctx,
1057        ) {
1058            refuse_remaining(&mut results, &refusal);
1059            return Ok(results);
1060        }
1061    }
1062
1063    let scripts = ReceiveHookScripts::new(repo, &request.push_options, shallow.hook_env()?);
1064    let all_cmds: Vec<&RefCommand> = request.commands.iter().collect();
1065    let pre_result = scripts.pre_receive(&all_cmds, quarantine.as_deref());
1066    relay_hook_sideband(&mut sideband_out, &pre_result)?;
1067    hook_records.push(hook_record("pre-receive", &pre_result));
1068    if !pre_result.is_ok() {
1069        refuse_remaining(&mut results, &RefUpdateRefusal::PreReceiveHookDeclined);
1070        return Ok(results);
1071    }
1072
1073    if results.iter().all(|r| r.error.is_some()) {
1074        return Ok(results);
1075    }
1076
1077    let mut migrated_pack_keeps = Vec::new();
1078    if let Some(q) = quarantine {
1079        migrated_pack_keeps = q.migrate().map_err(ServeError::Repository)?;
1080        repo.odb().invalidate_packs();
1081    }
1082
1083    for (cmd, result) in request.commands.iter().zip(results.iter_mut()) {
1084        if result.error.is_some() {
1085            continue;
1086        }
1087        if let Err(refusal) = hook.update_ctx(cmd, repo.odb(), &hook_ctx) {
1088            result.error = Some(refusal);
1089            continue;
1090        }
1091        let update_result = scripts.update(cmd);
1092        relay_hook_sideband(&mut sideband_out, &update_result)?;
1093        hook_records.push(hook_record("update", &update_result));
1094        if !update_result.is_ok() {
1095            result.error = Some(RefUpdateRefusal::UpdateHookDeclined);
1096        }
1097    }
1098
1099    if atomic && results.iter().any(|r| r.error.is_some()) {
1100        refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1101        return Ok(results);
1102    }
1103
1104    let new_roots: HashSet<ObjectId> = shallow
1105        .roots
1106        .iter()
1107        .zip(&results)
1108        .filter(|(_, r)| r.error.is_none())
1109        .flat_map(|(roots, _)| roots.iter().copied())
1110        .collect();
1111    if let Err(e) = add_shallow_boundaries(repo.git_dir(), &new_roots) {
1112        let refusal = RefUpdateRefusal::RefStore(e.to_string());
1113        for (roots, result) in shallow.roots.iter().zip(results.iter_mut()) {
1114            if !roots.is_empty() && result.error.is_none() {
1115                result.error = Some(refusal.clone());
1116            }
1117        }
1118        if atomic {
1119            refuse_remaining(&mut results, &RefUpdateRefusal::AtomicTransactionFailed);
1120            return Ok(results);
1121        }
1122    }
1123
1124    let pending: Vec<(usize, crate::refs::store::RefUpdate)> = request
1125        .commands
1126        .iter()
1127        .zip(results.iter())
1128        .enumerate()
1129        .filter(|(_, (_, r))| r.error.is_none())
1130        .map(|(idx, (cmd, _))| (idx, receive_command_store_update(cmd)))
1131        .collect();
1132
1133    let store = repo.refs();
1134    let ref_lock_wait = store.ref_lock_wait();
1135    if atomic {
1136        let batch: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1137        if let Err(e) =
1138            crate::refs::commit_ref_updates_on_store_with_wait(store, &batch, || ref_lock_wait)
1139        {
1140            let refusal = RefUpdateRefusal::RefStore(e.to_string());
1141            for (idx, _) in pending {
1142                if results[idx].error.is_none() {
1143                    results[idx].error = Some(refusal.clone());
1144                }
1145            }
1146        }
1147    } else {
1148        for (idx, message) in commit_receive_ref_updates_on_store(store, pending, ref_lock_wait) {
1149            results[idx].error = Some(RefUpdateRefusal::RefStore(message));
1150        }
1151    }
1152
1153    let successful: Vec<&RefCommand> = request
1154        .commands
1155        .iter()
1156        .zip(results.iter())
1157        .filter(|(_, r)| r.error.is_none())
1158        .map(|(c, _)| c)
1159        .collect();
1160    if !successful.is_empty() {
1161        let post_result = scripts.post_receive(&successful);
1162        relay_hook_sideband(&mut sideband_out, &post_result)?;
1163        hook_records.push(hook_record("post-receive", &post_result));
1164
1165        let pu_result = scripts.post_update(&successful);
1166        relay_hook_sideband(&mut sideband_out, &pu_result)?;
1167        hook_records.push(hook_record("post-update", &pu_result));
1168    }
1169
1170    drop(migrated_pack_keeps);
1171
1172    Ok(results)
1173}
1174
1175/// Refuse every update that has not been refused yet.
1176fn refuse_remaining(results: &mut [RefUpdateResult], reason: &RefUpdateRefusal) {
1177    for r in results.iter_mut().filter(|r| r.error.is_none()) {
1178        r.error = Some(reason.clone());
1179    }
1180}
1181
1182fn hook_record(name: &str, result: &HookResult) -> HookRunRecord {
1183    HookRunRecord {
1184        name: name.to_owned(),
1185        ran: result.was_executed(),
1186        exit_code: result.status(),
1187    }
1188}
1189
1190/// The receiving repository's hook scripts for one push, run with Git's arguments, stdin
1191/// and environment.
1192///
1193/// Every script runs from the repository directory with `GIT_DIR=.` (see
1194/// [`run_hook_in_git_dir`]) and its output captured. `pre-receive` and `post-receive` get the
1195/// push options as `GIT_PUSH_OPTION_COUNT` / `GIT_PUSH_OPTION_<n>`; `update` and
1196/// `post-update` do not, as in Git. Each `*_env` pair list also carries the extra
1197/// variables given to [`Self::new`] (such as `GIT_SHALLOW_FILE`).
1198pub(crate) struct ReceiveHookScripts<'a> {
1199    repo: &'a Repository,
1200    /// Push options plus the extra variables, for `pre-receive` and `post-receive`.
1201    receive_env: Vec<(String, String)>,
1202    /// The extra variables alone, for `update` and `post-update`.
1203    update_env: Vec<(String, String)>,
1204}
1205
1206impl<'a> ReceiveHookScripts<'a> {
1207    /// Scripts of `repo` for a push that sent `push_options`, with `extra_env` set for every
1208    /// script.
1209    pub(crate) fn new(
1210        repo: &'a Repository,
1211        push_options: &[String],
1212        extra_env: Vec<(String, String)>,
1213    ) -> Self {
1214        let mut receive_env = Vec::with_capacity(push_options.len() + 1 + extra_env.len());
1215        receive_env.push((
1216            "GIT_PUSH_OPTION_COUNT".to_owned(),
1217            push_options.len().to_string(),
1218        ));
1219        receive_env.extend(
1220            push_options
1221                .iter()
1222                .enumerate()
1223                .map(|(i, opt)| (format!("GIT_PUSH_OPTION_{i}"), opt.clone())),
1224        );
1225        receive_env.extend(extra_env.iter().cloned());
1226        Self {
1227            repo,
1228            receive_env,
1229            update_env: extra_env,
1230        }
1231    }
1232
1233    /// Run `pre-receive` over `commands` (one `<old> <new> <ref>` line each on stdin).
1234    ///
1235    /// With `quarantine`, the script also gets its variables so it can read the pushed
1236    /// objects before they enter the repository.
1237    pub(crate) fn pre_receive(
1238        &self,
1239        commands: &[&RefCommand],
1240        quarantine: Option<&ReceiveQuarantine>,
1241    ) -> HookResult {
1242        let mut env = self.receive_env.clone();
1243        if let Some(q) = quarantine {
1244            env.extend(q.hook_env());
1245        }
1246        self.run_with_commands("pre-receive", commands, &env)
1247    }
1248
1249    /// Run `update` for one command, with `<ref> <old> <new>` as arguments.
1250    pub(crate) fn update(&self, cmd: &RefCommand) -> HookResult {
1251        let algo = self.repo.odb().hash_algo();
1252        let hex = |oid: Option<ObjectId>| oid.unwrap_or_else(|| ObjectId::null(algo)).to_hex();
1253        let (old, new) = (hex(cmd.old), hex(cmd.new));
1254        let args = [cmd.refname.as_str(), old.as_str(), new.as_str()];
1255        run_hook_in_git_dir(self.repo, "update", &args, None, &pairs(&self.update_env))
1256    }
1257
1258    /// Run `post-receive` over the applied `commands`.
1259    pub(crate) fn post_receive(&self, commands: &[&RefCommand]) -> HookResult {
1260        self.run_with_commands("post-receive", commands, &self.receive_env)
1261    }
1262
1263    /// Run `post-update` with the names of the applied `commands` as arguments.
1264    pub(crate) fn post_update(&self, commands: &[&RefCommand]) -> HookResult {
1265        let args: Vec<&str> = commands.iter().map(|c| c.refname.as_str()).collect();
1266        run_hook_in_git_dir(
1267            self.repo,
1268            "post-update",
1269            &args,
1270            None,
1271            &pairs(&self.update_env),
1272        )
1273    }
1274
1275    fn run_with_commands(
1276        &self,
1277        hook_name: &str,
1278        commands: &[&RefCommand],
1279        env: &[(String, String)],
1280    ) -> HookResult {
1281        let zero = ObjectId::null(self.repo.odb().hash_algo());
1282        let stdin: String = commands
1283            .iter()
1284            .map(|c| {
1285                format!(
1286                    "{} {} {}\n",
1287                    c.old.unwrap_or(zero).to_hex(),
1288                    c.new.unwrap_or(zero).to_hex(),
1289                    c.refname
1290                )
1291            })
1292            .collect();
1293        run_hook_in_git_dir(
1294            self.repo,
1295            hook_name,
1296            &[],
1297            Some(stdin.as_bytes()),
1298            &pairs(env),
1299        )
1300    }
1301}
1302
1303fn pairs(env: &[(String, String)]) -> Vec<(&str, &str)> {
1304    env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
1305}
1306
1307fn receive_updates_eligible_for_initial_packed(updates: &[crate::refs::store::RefUpdate]) -> bool {
1308    use crate::refs::store::{Expected, RawRef};
1309    updates.iter().all(|update| {
1310        matches!(update.new_value, Some(RawRef::Direct(_)))
1311            && matches!(update.expected, Expected::Missing)
1312            && !update.flags.log_only
1313            && update.reflog.is_none()
1314    })
1315}
1316
1317/// Apply a non-atomic receive-pack ref batch in one store transaction when possible.
1318///
1319/// Plain creates use the initial packed-refs path; mixed batches use a regular transaction.
1320/// When a batch fails, fall back to a non-atomic batch (one reftable table write per phase)
1321/// so sibling updates still commit while individual refs can be rejected.
1322fn commit_receive_ref_updates_on_store(
1323    store: &dyn crate::refs::store::RefStore,
1324    pending: Vec<(usize, crate::refs::store::RefUpdate)>,
1325    ref_lock_wait: crate::lockfile::LockWait,
1326) -> Vec<(usize, String)> {
1327    if pending.is_empty() {
1328        return Vec::new();
1329    }
1330    let updates: Vec<_> = pending.iter().map(|(_, u)| u.clone()).collect();
1331    let batch_ok = if receive_updates_eligible_for_initial_packed(&updates) {
1332        crate::refs::commit_initial_ref_updates_on_store_with_wait(store, updates.clone(), || {
1333            ref_lock_wait
1334        })
1335    } else {
1336        crate::refs::commit_ref_updates_on_store_with_wait(store, &updates, || ref_lock_wait)
1337    };
1338    if batch_ok.is_ok() {
1339        return Vec::new();
1340    }
1341    let indices: Vec<_> = pending.iter().map(|(idx, _)| *idx).collect();
1342    match crate::refs::commit_ref_updates_non_atomic_on_store(store, updates) {
1343        Ok(outcomes) => indices
1344            .into_iter()
1345            .zip(outcomes)
1346            .filter_map(|(idx, outcome)| outcome.result.err().map(|e| (idx, e.to_string())))
1347            .collect(),
1348        Err(e) => indices
1349            .into_iter()
1350            .map(|idx| (idx, e.to_string()))
1351            .collect(),
1352    }
1353}
1354
1355fn receive_command_store_update(cmd: &RefCommand) -> crate::refs::store::RefUpdate {
1356    let mut flags = crate::refs::store::RefUpdateFlags::default();
1357    if cmd.refname == "HEAD" {
1358        flags.no_deref = true;
1359    }
1360    crate::refs::store::RefUpdate {
1361        name: cmd.refname.clone(),
1362        new_value: cmd.new.map(crate::refs::store::RawRef::Direct),
1363        expected: match cmd.old {
1364            Some(oid) => crate::refs::store::Expected::Oid(oid),
1365            None => crate::refs::store::Expected::Missing,
1366        },
1367        reflog: None,
1368        flags,
1369    }
1370}
1371
1372/// Relay what a hook wrote (stdout, then stderr) to the client on sideband 2.
1373fn relay_hook_sideband(out: &mut Option<&mut dyn Write>, result: &HookResult) -> Result<()> {
1374    let (Some(w), Some(output)) = (out.as_mut(), result.output()) else {
1375        return Ok(());
1376    };
1377    for chunk in output
1378        .stdout
1379        .chunks(65515)
1380        .chain(output.stderr.chunks(65515))
1381    {
1382        crate::pkt_line::write_sideband_packet(*w, 2, chunk).map_err(ServeError::Io)?;
1383    }
1384    Ok(())
1385}
1386
1387/// Encode the status report as pkt-lines: the unpack result, one line per ref,
1388/// and a closing flush.
1389fn build_report(
1390    unpack_error: Option<&UnpackError>,
1391    updates: &[RefUpdateResult],
1392) -> Result<Vec<u8>> {
1393    let mut report = Vec::new();
1394    match unpack_error {
1395        None => write_line(&mut report, "unpack ok")?,
1396        Some(e) => write_line(&mut report, &format!("unpack {}", one_line(&e.to_string())))?,
1397    }
1398    for u in updates {
1399        match &u.error {
1400            None => write_line(&mut report, &format!("ok {}", u.refname))?,
1401            Some(reason) => write_line(
1402                &mut report,
1403                &format!("ng {} {}", u.refname, one_line(&reason.to_string())),
1404            )?,
1405        }
1406    }
1407    write_flush(&mut report)?;
1408    Ok(report)
1409}
1410
1411/// Collapse a message to a single line so it fits in one status pkt-line.
1412fn one_line(msg: &str) -> String {
1413    msg.lines().next().unwrap_or_default().trim().to_owned()
1414}
1415
1416/// A pushed pack written to a temp file in the quarantine's `pack/` directory.
1417struct SpooledPack {
1418    file: tempfile::NamedTempFile,
1419    /// The 12-byte pack header (signature, version, object count).
1420    header: [u8; 12],
1421}
1422
1423/// Stream exactly one packfile from the push stream into a temp file.
1424///
1425/// The pack's end is found by scanning its entries as they arrive, and nothing
1426/// past the trailing checksum is requested: the push client keeps its write
1427/// half open until it has read the status report, so waiting for end of input
1428/// would hang (issue #1656). The checksum itself is verified when the pack is
1429/// stored.
1430fn spool_push_pack(
1431    input: &mut dyn Read,
1432    odb: &Odb,
1433    max_bytes: Option<u64>,
1434    stop: &CancelSignal<'_>,
1435) -> std::result::Result<SpooledPack, UnpackError> {
1436    let hash = odb.hash_algo();
1437    let mut file = tempfile::Builder::new()
1438        .prefix("tmp_pack_")
1439        .tempfile_in(odb.objects_dir().join("pack"))?;
1440    let mut scanner = PackStreamScanner::new(hash);
1441    let mut header = [0u8; 12];
1442    let mut chunk = vec![0u8; SPOOL_CHUNK];
1443    let mut written = 0u64;
1444    let mut pack_len: Option<u64> = None;
1445    let mut out = BufWriter::with_capacity(SPOOL_CHUNK, file.as_file_mut());
1446    loop {
1447        stop.poll().map_err(|_| UnpackError::Cancelled)?;
1448        let want = match pack_len {
1449            Some(len) if written >= len => break,
1450            Some(len) => usize::try_from(len - written).map_or(chunk.len(), |n| n.min(chunk.len())),
1451            None => chunk.len(),
1452        };
1453        let n = read_some(input, &mut chunk[..want])?;
1454        if n == 0 {
1455            return Err(UnpackError::Truncated);
1456        }
1457        let mut data = &chunk[..n];
1458        if pack_len.is_none() {
1459            scanner.feed(data)?;
1460            if scanner.is_complete() {
1461                let len = scanner
1462                    .pack_body_byte_len()
1463                    .saturating_add(hash.len() as u64);
1464                let keep = usize::try_from(len.saturating_sub(written)).map_or(n, |k| k.min(n));
1465                data = &data[..keep];
1466                pack_len = Some(len);
1467            }
1468        }
1469        if let Some(limit) = max_bytes {
1470            let total = pack_len.unwrap_or(written.saturating_add(data.len() as u64));
1471            if total > limit {
1472                return Err(UnpackError::TooLarge { limit });
1473            }
1474        }
1475        if let Some(missing) = usize::try_from(written)
1476            .ok()
1477            .and_then(|start| header.get_mut(start..))
1478        {
1479            let take = missing.len().min(data.len());
1480            missing[..take].copy_from_slice(&data[..take]);
1481        }
1482        out.write_all(data)?;
1483        written += data.len() as u64;
1484    }
1485    out.flush()?;
1486    drop(out);
1487    Ok(SpooledPack { file, header })
1488}
1489
1490/// `Read::read`, retrying when interrupted by a signal.
1491fn read_some(input: &mut dyn Read, buf: &mut [u8]) -> std::io::Result<usize> {
1492    loop {
1493        match input.read(buf) {
1494            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1495            other => return other,
1496        }
1497    }
1498}
1499
1500/// Check the objects a push stored in the quarantine `odb` against `fsck`.
1501///
1502/// `indexed` lists the objects of an installed pack; `None` means the pack was
1503/// exploded into loose objects, which are then all the quarantine holds.
1504fn check_received_objects(
1505    repo: &Repository,
1506    odb: &Odb,
1507    indexed: Option<&HashSet<ObjectId>>,
1508    cfg: &ConfigSet,
1509    fsck: &ObjectFsckPolicy,
1510) -> std::result::Result<(), UnpackError> {
1511    let checker = fsck.checker(odb.hash_algo(), repo.git_dir(), repo.environment())?;
1512    let mut received: Vec<ObjectId> = match indexed {
1513        Some(ids) => ids.iter().copied().collect(),
1514        None => odb
1515            .enumerate_local_loose_objects()?
1516            .into_iter()
1517            .map(|(oid, _)| oid)
1518            .collect(),
1519    };
1520    received.sort_unstable();
1521    let parallel = repo
1522        .thread_policy()
1523        .try_resolve(ParallelOp::IndexPack, cfg)
1524        .map_err(crate::error::Error::from)?;
1525    let verdicts = try_par_map(&received, parallel, received.len(), |oid| {
1526        if odb.read_info(oid)?.kind == ObjectKind::Blob {
1527            return Ok(None);
1528        }
1529        let obj = odb.read(oid)?;
1530        Ok(checker
1531            .check(oid, obj.kind, &obj.data)
1532            .err()
1533            .map(|error| RejectedObject {
1534                oid: *oid,
1535                kind: obj.kind,
1536                error,
1537            }))
1538    })
1539    .map_err(|ParallelHashError::Task(e): ParallelHashError<crate::error::Error>| e)?;
1540    match verdicts.into_iter().flatten().next() {
1541        Some(rejected) => Err(UnpackError::Rejected(rejected)),
1542        None => Ok(()),
1543    }
1544}
1545
1546/// The new tips among `commands` that reach an object that is in neither the push
1547/// (`received`, read through the quarantine `odb`) nor `repo`, unless a promisor
1548/// remote promises it, with the first such object. The walk stops at what
1549/// `existing` (the values of `repo`'s refs and `HEAD` before the push) reaches
1550/// (see [`super::connectivity`]).
1551fn unconnected_tips(
1552    repo: &Repository,
1553    odb: &Odb,
1554    received: &HashSet<ObjectId>,
1555    existing: &HashSet<ObjectId>,
1556    commands: &[RefCommand],
1557    shallow_boundaries: &HashSet<ObjectId>,
1558    promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1559) -> std::result::Result<HashMap<ObjectId, Unconnected>, UnpackError> {
1560    let mut tips: Vec<ObjectId> = commands.iter().filter_map(|c| c.new).collect();
1561    tips.sort_unstable();
1562    tips.dedup();
1563    if tips.is_empty() {
1564        return Ok(HashMap::new());
1565    }
1566    let receiver_grafts = load_shallow_boundaries(repo.git_dir());
1567    let graph = repo.odb().commit_graph_chain();
1568    let pushed = PushedObjects {
1569        odb,
1570        repo_odb: repo.odb(),
1571        graph: graph.as_deref(),
1572        received,
1573        existing,
1574        shallow_boundaries,
1575        receiver_grafts: &receiver_grafts,
1576        promises,
1577    };
1578    let gaps = pushed.unconnected_tips(&tips)?;
1579    Ok(tips
1580        .into_iter()
1581        .zip(gaps)
1582        .filter_map(|(tip, gap)| Some((tip, gap?)))
1583        .collect())
1584}
1585
1586/// The values of every ref of `repo` and of `HEAD`, with tags also peeled.
1587fn existing_ref_values(repo: &Repository) -> Result<HashSet<ObjectId>> {
1588    let mut existing = HashSet::new();
1589    for r in advertised_refs(repo, &[], &[], Peel::Yes)? {
1590        existing.insert(r.oid);
1591        existing.extend(r.peeled);
1592    }
1593    existing.extend(crate::refs::resolve_ref(repo.git_dir(), "HEAD").ok());
1594    Ok(existing)
1595}
1596
1597/// Store a spooled push pack: explode small packs into loose objects, index and
1598/// install larger ones, per `receive.unpacklimit`.
1599///
1600/// Returns the ids of the installed pack's objects; `None` when the objects
1601/// were unpacked as loose files.
1602///
1603/// The loose path checks that every pushed object's references resolve,
1604/// except the parents of `shallow_boundaries` (the pushing client is shallow
1605/// there and does not have them) and, with `promises`, objects a promisor
1606/// remote of the repository has: Git's connectivity check skips those too, so a
1607/// push to a repository whose large blobs live on a promisor works.
1608fn ingest_spooled_pack(
1609    odb: &Odb,
1610    pack: SpooledPack,
1611    cfg: &ConfigSet,
1612    shallow_boundaries: &HashSet<ObjectId>,
1613    promises: Option<&super::promised_objects::PromisorObjects<'_>>,
1614) -> std::result::Result<Option<HashSet<ObjectId>>, UnpackError> {
1615    if should_use_unpack_objects(&pack.header, cfg) {
1616        let mut reader = BufReader::with_capacity(SPOOL_CHUNK, pack.file.reopen()?);
1617        let opts = UnpackOptions {
1618            quiet: true,
1619            strict: true,
1620            shallow_boundaries: shallow_boundaries.clone(),
1621            ..UnpackOptions::default()
1622        };
1623        match promises {
1624            Some(promises) => {
1625                let promised =
1626                    |oid: &ObjectId, referrer: &ObjectId| promises.is_promised(oid, Some(referrer));
1627                crate::unpack_objects::unpack_objects_with_promises(
1628                    &mut reader,
1629                    odb,
1630                    &opts,
1631                    &promised,
1632                )?;
1633            }
1634            None => {
1635                unpack_objects(&mut reader, odb, &opts)?;
1636            }
1637        }
1638        return Ok(None);
1639    }
1640    let path = pack.file.into_temp_path();
1641    let installed = install_pack_path(
1642        &path,
1643        odb,
1644        &IngestPackOptions {
1645            fix_thin: true,
1646            // Indexing hashes every object and the trailer is checked while
1647            // installing, so a second full read of the pack proves nothing.
1648            skip_post_index_verify: true,
1649            merged_config: Some(cfg.clone()),
1650            ..Default::default()
1651        },
1652    )?;
1653    Ok(Some(installed.object_ids))
1654}
1655
1656#[cfg(test)]
1657mod tests {
1658    use super::*;
1659
1660    #[test]
1661    fn report_lists_unpack_status_then_each_ref() {
1662        let oid = ObjectId::from_hex("1111111111111111111111111111111111111111")
1663            .unwrap_or_else(|_| ObjectId::zero());
1664        let updates = vec![
1665            RefUpdateResult {
1666                refname: "refs/heads/main".into(),
1667                old: None,
1668                new: Some(oid),
1669                error: None,
1670            },
1671            RefUpdateResult {
1672                refname: "refs/heads/topic".into(),
1673                old: Some(oid),
1674                new: None,
1675                error: Some(RefUpdateRefusal::DeletionProhibited),
1676            },
1677        ];
1678        let report = build_report(None, &updates).unwrap_or_default();
1679        assert_eq!(
1680            String::from_utf8_lossy(&report),
1681            "000eunpack ok\n0017ok refs/heads/main\n002cng refs/heads/topic deletion prohibited\n0000"
1682        );
1683    }
1684
1685    #[test]
1686    fn report_renders_typed_unpack_error_and_refusal() {
1687        let updates = vec![RefUpdateResult {
1688            refname: "refs/heads/main".into(),
1689            old: None,
1690            new: None,
1691            error: Some(RefUpdateRefusal::UnpackerError),
1692        }];
1693        let report = build_report(Some(&UnpackError::Truncated), &updates).unwrap_or_default();
1694        assert_eq!(
1695            String::from_utf8_lossy(&report),
1696            "0029unpack unexpected end of pack stream\n0026ng refs/heads/main unpacker error\n0000"
1697        );
1698    }
1699
1700    #[test]
1701    fn refusals_render_the_reasons_git_clients_expect() {
1702        let cases = [
1703            (RefUpdateRefusal::InvalidRefName, "funny refname"),
1704            (RefUpdateRefusal::HiddenRef, "deny updating a hidden ref"),
1705            (RefUpdateRefusal::StaleInfo, "stale info"),
1706            (RefUpdateRefusal::DeletionProhibited, "deletion prohibited"),
1707            (
1708                RefUpdateRefusal::CurrentBranchDeletion,
1709                "deletion of the current branch prohibited",
1710            ),
1711            (
1712                RefUpdateRefusal::CurrentBranch,
1713                "branch is currently checked out",
1714            ),
1715            (
1716                RefUpdateRefusal::MissingObjects,
1717                "missing necessary objects",
1718            ),
1719            (
1720                RefUpdateRefusal::Unconnected {
1721                    missing: ObjectId::zero(),
1722                    referrer: ObjectId::zero(),
1723                },
1724                "missing necessary objects",
1725            ),
1726            (RefUpdateRefusal::NonFastForward, "non-fast-forward"),
1727            (RefUpdateRefusal::BadRef, "bad ref"),
1728            (
1729                RefUpdateRefusal::FastForwardCheckFailed,
1730                "unable to verify fast-forward",
1731            ),
1732            (
1733                RefUpdateRefusal::ShallowUpdateNotAllowed,
1734                "shallow update not allowed",
1735            ),
1736            (RefUpdateRefusal::UnpackerError, "unpacker error"),
1737            (
1738                RefUpdateRefusal::AtomicTransactionFailed,
1739                "atomic transaction failed",
1740            ),
1741            (
1742                RefUpdateRefusal::PreReceiveHookDeclined,
1743                "pre-receive hook declined",
1744            ),
1745            (RefUpdateRefusal::UpdateHookDeclined, "hook declined"),
1746            (RefUpdateRefusal::Policy("frozen".into()), "frozen"),
1747            (
1748                RefUpdateRefusal::TargetBranch {
1749                    target: "main".into(),
1750                },
1751                "main is the target branch; it changes only through integrate",
1752            ),
1753            (
1754                RefUpdateRefusal::RefStore("locked".into()),
1755                "failed to update ref: locked",
1756            ),
1757        ];
1758        for (refusal, wire) in cases {
1759            assert_eq!(refusal.to_string(), wire);
1760        }
1761    }
1762
1763    /// Serves `data`, then fails any further read: reading past the pack would
1764    /// block forever on a real push connection.
1765    struct NoReadPastEnd<'a> {
1766        data: &'a [u8],
1767    }
1768
1769    impl Read for NoReadPastEnd<'_> {
1770        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1771            if self.data.is_empty() {
1772                return Err(std::io::Error::other("read past the end of the pack"));
1773            }
1774            let n = buf.len().min(self.data.len());
1775            buf[..n].copy_from_slice(&self.data[..n]);
1776            self.data = &self.data[n..];
1777            Ok(n)
1778        }
1779    }
1780
1781    fn sample_pack() -> Vec<u8> {
1782        use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
1783        let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
1784        let mut seed = 0x2545_f491_u32;
1785        // Incompressible, so the pack spans several spool chunks.
1786        let big: Vec<u8> = (0..(SPOOL_CHUNK * 3))
1787            .map(|_| {
1788                seed ^= seed << 13;
1789                seed ^= seed >> 17;
1790                seed ^= seed << 5;
1791                seed as u8
1792            })
1793            .collect();
1794        pb.add_full(PK::Blob, &big);
1795        pb.add_full(PK::Blob, b"small\n");
1796        pb.build().bytes
1797    }
1798
1799    fn test_odb(dir: &std::path::Path) -> Odb {
1800        let objects = dir.join("objects");
1801        std::fs::create_dir_all(objects.join("pack")).unwrap_or_default();
1802        Odb::new(&objects)
1803    }
1804
1805    #[test]
1806    fn spool_stops_at_the_trailer_without_reading_further() {
1807        let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1808        let odb = test_odb(tmp.path());
1809        let pack = sample_pack();
1810        let mut input = NoReadPastEnd { data: &pack };
1811        let spooled = spool_push_pack(&mut input, &odb, None, &CancelSignal::never())
1812            .unwrap_or_else(|e| panic!("{e}"));
1813        assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1814        assert_eq!(&spooled.header[..], &pack[..12]);
1815    }
1816
1817    #[test]
1818    fn spool_keeps_only_the_pack_when_more_bytes_follow() {
1819        let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1820        let odb = test_odb(tmp.path());
1821        let pack = sample_pack();
1822        let mut stream = pack.clone();
1823        stream.extend_from_slice(b"trailing junk");
1824        let spooled = spool_push_pack(&mut stream.as_slice(), &odb, None, &CancelSignal::never())
1825            .unwrap_or_else(|e| panic!("{e}"));
1826        assert_eq!(std::fs::read(spooled.file.path()).unwrap_or_default(), pack);
1827    }
1828
1829    #[test]
1830    fn spool_reports_typed_errors() {
1831        let tmp = tempfile::tempdir().unwrap_or_else(|e| panic!("{e}"));
1832        let odb = test_odb(tmp.path());
1833        let pack = sample_pack();
1834
1835        let truncated = &pack[..pack.len() - 5];
1836        let never = CancelSignal::never();
1837        assert!(matches!(
1838            spool_push_pack(&mut &truncated[..], &odb, None, &never),
1839            Err(UnpackError::Truncated)
1840        ));
1841
1842        let limit = (pack.len() - 1) as u64;
1843        assert!(matches!(
1844            spool_push_pack(&mut pack.as_slice(), &odb, Some(limit), &never),
1845            Err(UnpackError::TooLarge { limit: l }) if l == limit
1846        ));
1847        assert!(
1848            spool_push_pack(&mut pack.as_slice(), &odb, Some(pack.len() as u64), &never).is_ok()
1849        );
1850
1851        assert!(matches!(
1852            spool_push_pack(&mut &b"JUNK\0\0\0\x02\0\0\0\x01"[..], &odb, None, &never),
1853            Err(UnpackError::Malformed(PackStreamScanError::BadSignature))
1854        ));
1855
1856        let leftovers: Vec<_> = std::fs::read_dir(odb.objects_dir().join("pack"))
1857            .map(|rd| rd.filter_map(|e| e.ok()).collect())
1858            .unwrap_or_default();
1859        assert!(
1860            leftovers.is_empty(),
1861            "failed spools must not leave temp files"
1862        );
1863    }
1864
1865    #[test]
1866    fn deny_current_branch_values() {
1867        let policy_for = |value: Option<&str>| {
1868            let mut config = ConfigSet::new();
1869            if let Some(v) = value {
1870                let _ = config.add_command_override("receive.denyCurrentBranch", v);
1871            }
1872            ReceivePolicy::from_config(&config).deny_current_branch
1873        };
1874        assert!(policy_for(None));
1875        assert!(!policy_for(Some("ignore")));
1876        assert!(!policy_for(Some("updateInstead")));
1877        assert!(policy_for(Some("refuse")));
1878    }
1879
1880    #[test]
1881    fn shallow_update_is_off_unless_configured() {
1882        assert!(!ReceivePolicy::default().shallow_update);
1883        let mut config = ConfigSet::new();
1884        let _ = config.add_command_override("receive.shallowUpdate", "true");
1885        assert!(ReceivePolicy::from_config(&config).shallow_update);
1886    }
1887
1888    #[test]
1889    fn deny_delete_current_values() {
1890        let policy_for = |value: Option<&str>| {
1891            let mut config = ConfigSet::new();
1892            if let Some(v) = value {
1893                let _ = config.add_command_override("receive.denyDeleteCurrent", v);
1894            }
1895            ReceivePolicy::from_config(&config).deny_delete_current
1896        };
1897        assert!(policy_for(None));
1898        assert!(!policy_for(Some("ignore")));
1899        assert!(!policy_for(Some("warn")));
1900        assert!(!policy_for(Some("false")));
1901        // There is no working tree to update for a deleted branch.
1902        assert!(policy_for(Some("updateInstead")));
1903        assert!(policy_for(Some("refuse")));
1904        assert!(ReceivePolicy::default().deny_delete_current);
1905    }
1906}