Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-wire/src/filter.rs

//! Partial clone, via the `filter <spec>` argument.
//!
//! Advertised because it is implemented, and implemented because it is
//! advertised. The one thing this must never be is a parser that accepts a
//! specification and then packs everything anyway: a client asking for
//! `blob:none` and receiving every blob gets a working clone, so no test that
//! only checks the clone succeeded can tell the difference.

use crate::{Error, Result};

/// A partial-clone filter gunnar implements.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Filter {
    /// `blob:none`: trees and commits only.
    BlobNone,
    /// `blob:limit=<n>` omits blobs strictly larger than `n` bytes.
    BlobLimit(u64),
    /// `tree:<depth>` omits trees below `depth`, and every blob under them.
    /// `tree:0` yields no trees or blobs at all.
    TreeDepth(usize),
}

impl Filter {
    /// Parse git's filter-spec syntax.
    ///
    /// The unsupported forms are refused by name rather than ignored: a
    /// silently-ignored `sparse:oid` filter is a client that believes it made a
    /// partial clone and did not.
    pub fn parse(spec: &str) -> Result<Self> {
        let unsupported = |reason| Error::UnsupportedFilter {
            spec: spec.to_owned(),
            reason,
        };
        if spec == "blob:none" {
            return Ok(Filter::BlobNone);
        }
        if let Some(limit) = spec.strip_prefix("blob:limit=") {
            return parse_size(limit).map(Filter::BlobLimit).ok_or_else(|| {
                unsupported("the size is not a number with an optional k/m/g suffix")
            });
        }
        if let Some(depth) = spec.strip_prefix("tree:") {
            return depth
                .parse::<usize>()
                .map(Filter::TreeDepth)
                .map_err(|_| unsupported("the depth is not a number"));
        }
        if spec.starts_with("combine:") {
            return Err(unsupported("gunnar implements one filter at a time"));
        }
        Err(unsupported(
            "gunnar implements blob:none, blob:limit=<n> and tree:<depth>",
        ))
    }

    /// Is a tree at this depth (a commit's root tree being depth 1) kept?
    pub(crate) fn keeps_tree(&self, depth: usize) -> bool {
        match self {
            Filter::BlobNone | Filter::BlobLimit(_) => true,
            Filter::TreeDepth(max) => depth <= *max,
        }
    }

    /// The size ceiling for blobs, if this filter has one.
    pub(crate) fn blob_limit(&self) -> Option<u64> {
        match self {
            Filter::BlobLimit(limit) => Some(*limit),
            _ => None,
        }
    }

    /// Are blobs kept at all, for the filters that decide without looking at
    /// size?
    pub(crate) fn keeps_any_blob(&self) -> bool {
        match self {
            Filter::BlobNone => false,
            Filter::BlobLimit(_) => true,
            // A blob only ever appears under a tree that was kept, and
            // `tree:0` keeps no trees, so this follows the tree decision.
            Filter::TreeDepth(max) => *max > 0,
        }
    }
}

fn parse_size(text: &str) -> Option<u64> {
    let (digits, scale) = match text.as_bytes().last()? {
        b'k' | b'K' => (&text[..text.len() - 1], 1024),
        b'm' | b'M' => (&text[..text.len() - 1], 1024 * 1024),
        b'g' | b'G' => (&text[..text.len() - 1], 1024 * 1024 * 1024),
        _ => (text, 1),
    };
    digits.parse::<u64>().ok()?.checked_mul(scale)
}