Skip to main content

gix_ref/store/packed/
transaction.rs

1use std::{borrow::Cow, fmt::Formatter, io::Write};
2
3use crate::{
4    file,
5    store_impl::{packed, packed::Edit},
6    transaction::{Change, RefEdit},
7    Namespace, Target,
8};
9
10pub(crate) const HEADER_LINE: &[u8] = b"# pack-refs with: peeled fully-peeled sorted \n";
11
12/// Access and instantiation
13impl packed::Transaction {
14    pub(crate) fn new_from_pack_and_lock(
15        buffer: Option<file::packed::SharedBufferSnapshot>,
16        lock: gix_lock::File,
17        precompose_unicode: bool,
18        namespace: Option<Namespace>,
19    ) -> Self {
20        packed::Transaction {
21            buffer,
22            edits: None,
23            lock: Some(lock),
24            closed_lock: None,
25            precompose_unicode,
26            namespace,
27        }
28    }
29}
30
31impl std::fmt::Debug for packed::Transaction {
32    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("packed::Transaction")
34            .field("edits", &self.edits.as_ref().map(Vec::len))
35            .field("lock", &self.lock)
36            .finish_non_exhaustive()
37    }
38}
39
40/// Access
41impl packed::Transaction {
42    /// Returns our packed buffer
43    pub fn buffer(&self) -> Option<&packed::Buffer> {
44        self.buffer.as_ref().map(|b| &***b)
45    }
46}
47
48/// Lifecycle
49impl packed::Transaction {
50    /// Prepare the transaction by checking all edits for applicability.
51    /// Use `objects` to access objects for the purpose of peeling them - this is only used if packed-refs are involved.
52    pub fn prepare(
53        mut self,
54        edits: &mut dyn Iterator<Item = RefEdit>,
55        objects: &dyn gix_object::Find,
56    ) -> Result<Self, prepare::Error> {
57        assert!(self.edits.is_none(), "BUG: cannot call prepare(…) more than once");
58        let buffer = &self.buffer;
59        // Remove all edits which are deletions that aren't here in the first place
60        let mut edits: Vec<Edit> = edits
61            .into_iter()
62            .map(|mut edit| {
63                use gix_object::bstr::ByteSlice;
64                if self.precompose_unicode {
65                    let precomposed = edit
66                        .name
67                        .0
68                        .to_str()
69                        .ok()
70                        .map(|name| gix_utils::str::precompose(name.into()));
71                    match precomposed {
72                        None | Some(Cow::Borrowed(_)) => edit,
73                        Some(Cow::Owned(precomposed)) => {
74                            edit.name.0 = precomposed.into();
75                            edit
76                        }
77                    }
78                } else {
79                    edit
80                }
81            })
82            .map(|mut edit| {
83                if let Some(namespace) = &self.namespace {
84                    edit.name = namespace.clone().into_namespaced_name(edit.name.as_ref());
85                }
86                edit
87            })
88            .filter(|edit| {
89                if let Change::Delete { .. } = edit.change {
90                    buffer.as_ref().is_none_or(|b| b.find(edit.name.as_ref()).is_ok())
91                } else {
92                    true
93                }
94            })
95            .map(|change| Edit {
96                inner: change,
97                peeled: None,
98            })
99            .collect();
100
101        let mut buf = Vec::new();
102        for edit in &mut edits {
103            if let Change::Update {
104                new: Target::Object(new),
105                ..
106            } = edit.inner.change
107            {
108                let mut next_id = new;
109                edit.peeled = loop {
110                    let data = objects.try_find(&next_id, &mut buf)?;
111                    match data {
112                        Some(gix_object::Data {
113                            kind: gix_object::Kind::Tag,
114                            data,
115                            hash_kind,
116                        }) => {
117                            next_id = gix_object::TagRefIter::from_bytes(data, hash_kind)
118                                .target_id()
119                                .map_err(|_| {
120                                    prepare::Error::Resolve(
121                                        format!("Couldn't get target object id from tag {next_id}").into(),
122                                    )
123                                })?;
124                        }
125                        Some(_) => {
126                            break if next_id == new { None } else { Some(next_id) };
127                        }
128                        None => {
129                            return Err(prepare::Error::Resolve(
130                                format!("Couldn't find object with id {next_id}").into(),
131                            ))
132                        }
133                    }
134                };
135            }
136        }
137
138        if edits.is_empty() {
139            self.closed_lock = self
140                .lock
141                .take()
142                .map(gix_lock::File::close)
143                .transpose()
144                .map_err(prepare::Error::CloseLock)?;
145        } else {
146            // NOTE that we don't do any additional checks here but apply all edits unconditionally.
147            // This is because this transaction system is internal and will be used correctly from the
148            // loose ref store transactions, which do the necessary checking.
149        }
150        self.edits = Some(edits);
151        Ok(self)
152    }
153
154    /// Commit the prepared transaction.
155    ///
156    /// Please note that actual edits invalidated existing packed buffers.
157    /// Note: There is the potential to write changes into memory and return such a packed-refs buffer for reuse.
158    pub fn commit(self) -> Result<(), commit::Error> {
159        let mut edits = self.edits.expect("BUG: cannot call commit() before prepare(…)");
160        if edits.is_empty() {
161            return Ok(());
162        }
163
164        let mut file = self.lock.expect("a write lock for applying changes");
165        let refs_sorted: Box<dyn Iterator<Item = Result<packed::Reference<'_>, packed::iter::Error>>> =
166            match self.buffer.as_ref() {
167                Some(buffer) => Box::new(buffer.iter()?),
168                None => Box::new(std::iter::empty()),
169            };
170
171        let mut refs_sorted = refs_sorted.peekable();
172
173        edits.sort_by(|l, r| l.inner.name.as_bstr().cmp(r.inner.name.as_bstr()));
174        let mut peekable_sorted_edits = edits.iter().peekable();
175
176        file.with_mut(|f| f.write_all(HEADER_LINE))?;
177
178        let mut num_written_lines = 0;
179        loop {
180            match (refs_sorted.peek(), peekable_sorted_edits.peek()) {
181                (Some(Err(_)), _) => {
182                    let err = refs_sorted.next().expect("next").expect_err("err");
183                    return Err(commit::Error::Iteration(err));
184                }
185                (None, None) => {
186                    break;
187                }
188                (Some(Ok(_)), None) => {
189                    let pref = refs_sorted.next().expect("next").expect("no err");
190                    num_written_lines += 1;
191                    file.with_mut(|out| write_packed_ref(out, pref))?;
192                }
193                (Some(Ok(pref)), Some(edit)) => {
194                    use std::cmp::Ordering::*;
195                    match pref.name.as_bstr().cmp(edit.inner.name.as_bstr()) {
196                        Less => {
197                            let pref = refs_sorted.next().expect("next").expect("valid");
198                            num_written_lines += 1;
199                            file.with_mut(|out| write_packed_ref(out, pref))?;
200                        }
201                        Greater => {
202                            let edit = peekable_sorted_edits.next().expect("next");
203                            file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?;
204                        }
205                        Equal => {
206                            let _pref = refs_sorted.next().expect("next").expect("valid");
207                            let edit = peekable_sorted_edits.next().expect("next");
208                            file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?;
209                        }
210                    }
211                }
212                (None, Some(_)) => {
213                    let edit = peekable_sorted_edits.next().expect("next");
214                    file.with_mut(|out| write_edit(out, edit, &mut num_written_lines))?;
215                }
216            }
217        }
218
219        if num_written_lines == 0 {
220            std::fs::remove_file(file.resource_path())?;
221        } else {
222            file.commit()?;
223        }
224        drop(refs_sorted);
225        Ok(())
226    }
227}
228
229fn write_packed_ref(out: &mut dyn std::io::Write, pref: packed::Reference<'_>) -> std::io::Result<()> {
230    write!(out, "{} ", pref.target)?;
231    out.write_all(pref.name.as_bstr())?;
232    out.write_all(b"\n")?;
233    if let Some(object) = pref.object {
234        writeln!(out, "^{object}")?;
235    }
236    Ok(())
237}
238
239fn write_edit(out: &mut dyn std::io::Write, edit: &Edit, lines_written: &mut i32) -> std::io::Result<()> {
240    match edit.inner.change {
241        Change::Delete { .. } => {}
242        Change::Update {
243            new: Target::Object(target_oid),
244            ..
245        } => {
246            write!(out, "{target_oid} ")?;
247            out.write_all(edit.inner.name.as_bstr())?;
248            out.write_all(b"\n")?;
249            if let Some(object) = edit.peeled {
250                writeln!(out, "^{object}")?;
251            }
252            *lines_written += 1;
253        }
254        Change::Update {
255            new: Target::Symbolic(_),
256            ..
257        } => unreachable!("BUG: packed refs cannot contain symbolic refs, catch that in prepare(…)"),
258    }
259    Ok(())
260}
261
262/// Convert this buffer to be used as the basis for a transaction.
263pub(crate) fn buffer_into_transaction(
264    buffer: file::packed::SharedBufferSnapshot,
265    lock_mode: gix_lock::acquire::Fail,
266    precompose_unicode: bool,
267    namespace: Option<Namespace>,
268) -> Result<packed::Transaction, gix_lock::acquire::Error> {
269    let lock = gix_lock::File::acquire_to_update_resource(&buffer.path, lock_mode, None)?;
270    Ok(packed::Transaction {
271        buffer: Some(buffer),
272        lock: Some(lock),
273        closed_lock: None,
274        edits: None,
275        precompose_unicode,
276        namespace,
277    })
278}
279
280///
281pub mod prepare {
282    /// The error used in [`Transaction::prepare(…)`][crate::file::Transaction::prepare()].
283    #[derive(Debug, thiserror::Error)]
284    #[allow(missing_docs)]
285    pub enum Error {
286        #[error("Could not close a lock which won't ever be committed")]
287        CloseLock(#[from] std::io::Error),
288        #[error("The lookup of an object failed while peeling it")]
289        Resolve(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
290    }
291}
292
293///
294pub mod commit {
295    use crate::store_impl::packed;
296
297    /// The error used in [`Transaction::commit(…)`][crate::file::Transaction::commit()].
298    #[derive(Debug, thiserror::Error)]
299    #[allow(missing_docs)]
300    pub enum Error {
301        #[error("Changes to the resource could not be committed")]
302        Commit(#[from] gix_lock::commit::Error<gix_lock::File>),
303        #[error("Some references in the packed refs buffer could not be parsed")]
304        Iteration(#[from] packed::iter::Error),
305        #[error("Failed to write a ref line to the packed ref file")]
306        Io(#[from] std::io::Error),
307    }
308}