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
11impl 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
39impl packed::Transaction {
41 pub fn buffer(&self) -> Option<&packed::Buffer> {
43 self.buffer.as_ref().map(|b| &***b)
44 }
45}
46
47impl packed::Transaction {
49 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 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 }
149 self.edits = Some(edits);
150 Ok(self)
151 }
152
153 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
261pub(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
279pub mod prepare {
281 #[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
292pub mod commit {
294 use crate::store_impl::packed;
295
296 #[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}