Skip to main content

gunnar_wire/
filter.rs

1//! Partial clone, via the `filter <spec>` argument.
2//!
3//! Advertised because it is implemented, and implemented because it is
4//! advertised. The one thing this must never be is a parser that accepts a
5//! specification and then packs everything anyway: a client asking for
6//! `blob:none` and receiving every blob gets a working clone, so no test that
7//! only checks the clone succeeded can tell the difference.
8
9use crate::{Error, Result};
10
11/// A partial-clone filter gunnar implements.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Filter {
14    /// `blob:none`: trees and commits only.
15    BlobNone,
16    /// `blob:limit=<n>` omits blobs strictly larger than `n` bytes.
17    BlobLimit(u64),
18    /// `tree:<depth>` omits trees below `depth`, and every blob under them.
19    /// `tree:0` yields no trees or blobs at all.
20    TreeDepth(usize),
21}
22
23impl Filter {
24    /// Parse git's filter-spec syntax.
25    ///
26    /// The unsupported forms are refused by name rather than ignored: a
27    /// silently-ignored `sparse:oid` filter is a client that believes it made a
28    /// partial clone and did not.
29    pub fn parse(spec: &str) -> Result<Self> {
30        let unsupported = |reason| Error::UnsupportedFilter {
31            spec: spec.to_owned(),
32            reason,
33        };
34        if spec == "blob:none" {
35            return Ok(Filter::BlobNone);
36        }
37        if let Some(limit) = spec.strip_prefix("blob:limit=") {
38            return parse_size(limit).map(Filter::BlobLimit).ok_or_else(|| {
39                unsupported("the size is not a number with an optional k/m/g suffix")
40            });
41        }
42        if let Some(depth) = spec.strip_prefix("tree:") {
43            return depth
44                .parse::<usize>()
45                .map(Filter::TreeDepth)
46                .map_err(|_| unsupported("the depth is not a number"));
47        }
48        if spec.starts_with("combine:") {
49            return Err(unsupported("gunnar implements one filter at a time"));
50        }
51        Err(unsupported(
52            "gunnar implements blob:none, blob:limit=<n> and tree:<depth>",
53        ))
54    }
55
56    /// Is a tree at this depth (a commit's root tree being depth 1) kept?
57    pub(crate) fn keeps_tree(&self, depth: usize) -> bool {
58        match self {
59            Filter::BlobNone | Filter::BlobLimit(_) => true,
60            Filter::TreeDepth(max) => depth <= *max,
61        }
62    }
63
64    /// The size ceiling for blobs, if this filter has one.
65    pub(crate) fn blob_limit(&self) -> Option<u64> {
66        match self {
67            Filter::BlobLimit(limit) => Some(*limit),
68            _ => None,
69        }
70    }
71
72    /// Are blobs kept at all, for the filters that decide without looking at
73    /// size?
74    pub(crate) fn keeps_any_blob(&self) -> bool {
75        match self {
76            Filter::BlobNone => false,
77            Filter::BlobLimit(_) => true,
78            // A blob only ever appears under a tree that was kept, and
79            // `tree:0` keeps no trees, so this follows the tree decision.
80            Filter::TreeDepth(max) => *max > 0,
81        }
82    }
83}
84
85fn parse_size(text: &str) -> Option<u64> {
86    let (digits, scale) = match text.as_bytes().last()? {
87        b'k' | b'K' => (&text[..text.len() - 1], 1024),
88        b'm' | b'M' => (&text[..text.len() - 1], 1024 * 1024),
89        b'g' | b'G' => (&text[..text.len() - 1], 1024 * 1024 * 1024),
90        _ => (text, 1),
91    };
92    digits.parse::<u64>().ok()?.checked_mul(scale)
93}