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