Skip to main content

grit_lib/serve/
receive_pack.rs

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