gix_ref/store/packed/
transaction.rs

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