git_ref/store/packed/
transaction.rs

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