1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
use std::{collections::HashSet, ops::Deref};

use gix_object::Exists;

use crate::store::{load_index, Handle};

///
pub mod lookup {
    use crate::loose;

    /// Returned by [`Handle::lookup_prefix()`][crate::store::Handle::lookup_prefix()]
    #[derive(thiserror::Error, Debug)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error("An error occurred looking up a prefix which requires iteration")]
        LooseWalkDir(#[from] loose::iter::Error),
        #[error(transparent)]
        LoadIndex(#[from] crate::store::load_index::Error),
    }

    /// A way to indicate if a lookup, despite successful, was ambiguous or yielded exactly
    /// one result in the particular index.
    pub type Outcome = Result<gix_hash::ObjectId, ()>;
}

///
pub mod disambiguate {
    /// A potentially ambiguous prefix for use with `Handle::disambiguate_prefix()`.
    #[derive(Debug, Copy, Clone)]
    pub struct Candidate {
        id: gix_hash::ObjectId,
        hex_len: usize,
    }

    impl Candidate {
        /// Create a new potentially ambiguous prefix from an `id` and the desired minimal `hex_len`.
        ///
        /// It is considered ambiguous until it's disambiguated by validating that there is only a single object
        /// matching this prefix.
        pub fn new(id: impl Into<gix_hash::ObjectId>, hex_len: usize) -> Result<Self, gix_hash::prefix::Error> {
            let id = id.into();
            gix_hash::Prefix::new(&id, hex_len)?;
            Ok(Candidate { id, hex_len })
        }

        /// Transform ourselves into a `Prefix` with our current hex lengths.
        pub fn to_prefix(&self) -> gix_hash::Prefix {
            gix_hash::Prefix::new(&self.id, self.hex_len).expect("our hex-len to always be in bounds")
        }

        pub(crate) fn inc_hex_len(&mut self) {
            self.hex_len += 1;
            assert!(self.hex_len <= self.id.kind().len_in_hex());
        }

        pub(crate) fn id(&self) -> &gix_hash::oid {
            &self.id
        }

        pub(crate) fn hex_len(&self) -> usize {
            self.hex_len
        }
    }

    /// Returned by [`Handle::disambiguate_prefix()`][crate::store::Handle::disambiguate_prefix()]
    #[derive(thiserror::Error, Debug)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error("An error occurred while trying to determine if a full hash contained in the object database")]
        Contains(#[from] crate::store::find::Error),
        #[error(transparent)]
        Lookup(#[from] super::lookup::Error),
    }
}

impl<S> Handle<S>
where
    S: Deref<Target = super::Store> + Clone,
{
    /// Return the exact number of packed objects after loading all currently available indices
    /// as last seen on disk.
    pub fn packed_object_count(&self) -> Result<u64, load_index::Error> {
        let mut count = self.packed_object_count.borrow_mut();
        match *count {
            Some(count) => Ok(count),
            None => {
                let _span = gix_features::trace::detail!("gix_odb::Handle::packed_object_count()");
                let mut snapshot = self.snapshot.borrow_mut();
                *snapshot = self.store.load_all_indices()?;
                let mut obj_count = 0;
                for index in &snapshot.indices {
                    obj_count += index.num_objects() as u64;
                }
                *count = Some(obj_count);
                Ok(obj_count)
            }
        }
    }

    /// Given a prefix `candidate` with an object id and an initial `hex_len`, check if it only matches a single
    /// object within the entire object database and increment its `hex_len` by one until it is unambiguous.
    /// Return `Ok(None)` if no object with that prefix exists.
    pub fn disambiguate_prefix(
        &self,
        mut candidate: disambiguate::Candidate,
    ) -> Result<Option<gix_hash::Prefix>, disambiguate::Error> {
        let max_hex_len = candidate.id().kind().len_in_hex();
        if candidate.hex_len() == max_hex_len {
            return Ok(self.exists(candidate.id()).then(|| candidate.to_prefix()));
        }

        while candidate.hex_len() != max_hex_len {
            let res = self.lookup_prefix(candidate.to_prefix(), None)?;
            match res {
                Some(Ok(_id)) => return Ok(Some(candidate.to_prefix())),
                Some(Err(())) => {
                    candidate.inc_hex_len();
                    continue;
                }
                None => return Ok(None),
            }
        }
        Ok(Some(candidate.to_prefix()))
    }

    /// Find the only object matching `prefix` and return it as `Ok(Some(Ok(<ObjectId>)))`, or return `Ok(Some(Err(()))`
    /// if multiple different objects with the same prefix were found.
    ///
    /// Return `Ok(None)` if no object matched the `prefix`.
    ///
    /// Pass `candidates` to obtain the set of all object ids matching `prefix`, with the same return value as
    /// one would have received if it remained `None`.
    ///
    /// ### Performance Note
    ///
    /// - Unless the handles refresh mode is set to `Never`, each lookup will trigger a refresh of the object databases files
    ///   on disk if the prefix doesn't lead to ambiguous results.
    /// - Since all objects need to be examined to assure non-ambiguous return values, after calling this method all indices will
    ///   be loaded.
    /// - If `candidates` is `Some(…)`, the traversal will continue to obtain all candidates, which takes more time
    ///   as there is no early abort.
    pub fn lookup_prefix(
        &self,
        prefix: gix_hash::Prefix,
        mut candidates: Option<&mut HashSet<gix_hash::ObjectId>>,
    ) -> Result<Option<lookup::Outcome>, lookup::Error> {
        let mut candidate: Option<gix_hash::ObjectId> = None;
        loop {
            let snapshot = self.snapshot.borrow();
            for index in &snapshot.indices {
                #[allow(clippy::needless_option_as_deref)] // needed as it's the equivalent of a reborrow.
                let lookup_result = index.lookup_prefix(prefix, candidates.as_deref_mut());
                if candidates.is_none() && !check_candidate(lookup_result, &mut candidate) {
                    return Ok(Some(Err(())));
                }
            }

            for lodb in snapshot.loose_dbs.iter() {
                #[allow(clippy::needless_option_as_deref)] // needed as it's the equivalent of a reborrow.
                let lookup_result = lodb.lookup_prefix(prefix, candidates.as_deref_mut())?;
                if candidates.is_none() && !check_candidate(lookup_result, &mut candidate) {
                    return Ok(Some(Err(())));
                }
            }

            match self.store.load_one_index(self.refresh, snapshot.marker)? {
                Some(new_snapshot) => {
                    drop(snapshot);
                    *self.snapshot.borrow_mut() = new_snapshot;
                }
                None => {
                    return match &candidates {
                        Some(candidates) => match candidates.len() {
                            0 => Ok(None),
                            1 => Ok(candidates.iter().copied().next().map(Ok)),
                            _ => Ok(Some(Err(()))),
                        },
                        None => Ok(candidate.map(Ok)),
                    };
                }
            }
        }

        fn check_candidate(lookup_result: Option<lookup::Outcome>, candidate: &mut Option<gix_hash::ObjectId>) -> bool {
            match (lookup_result, &*candidate) {
                (Some(Ok(oid)), Some(candidate)) if *candidate != oid => false,
                (Some(Ok(_)) | None, Some(_)) | (None, None) => true,
                (Some(Err(())), _) => false,
                (Some(Ok(oid)), None) => {
                    *candidate = Some(oid);
                    true
                }
            }
        }
    }
}