Skip to main content

gix_ref/store/packed/
transaction.rs

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