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
12impl 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
40impl packed::Transaction {
42 pub fn buffer(&self) -> Option<&packed::Buffer> {
44 self.buffer.as_ref().map(|b| &***b)
45 }
46}
47
48impl packed::Transaction {
50 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 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 }
150 self.edits = Some(edits);
151 Ok(self)
152 }
153
154 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
262pub(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
280pub mod prepare {
282 #[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
293pub mod commit {
295 use crate::store_impl::packed;
296
297 #[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}