gix_ref/store/file/transaction/commit.rs
1use gix_error::{ErrorExt, ExnResult, Message, ResultExt, message};
2
3use crate::{
4 Target,
5 store_impl::file::{Transaction, transaction::PackedRefs},
6 transaction::{Change, LogChange, RefEdit, RefLog},
7};
8
9impl Transaction<'_, '_> {
10 /// Make all [prepared][Transaction::prepare()] permanent and return the performed edits which represent the current
11 /// state of the affected refs in the ref store in that instant. Please note that the obtained edits may have been
12 /// adjusted to contain more dependent edits or additional information.
13 /// `committer` is used in the reflog and only if the reflog is actually written, which is why it is optional. Please note
14 /// that if `None` is passed and the reflog needs to be written, the operation will be aborted late and a few refs may have been
15 /// successfully committed already, making clear the non-atomic nature of multi-file edits.
16 ///
17 /// On error the transaction may have been performed partially, depending on the nature of the error, and no attempt to roll back
18 /// partial changes is made.
19 ///
20 /// In this stage, we perform the following operations:
21 ///
22 /// * update the ref log
23 /// * move updated refs into place
24 /// * delete reflogs and empty parent directories
25 /// * delete packed refs
26 /// * delete their corresponding reference (if applicable)
27 /// along with empty parent directories
28 ///
29 /// Note that transactions will be prepared automatically as needed.
30 /// Per-reference failures include [metadata](gix_error::Exn::metadata()) `reference` (bytes), the affected name.
31 /// A missing reflog identity is identifiable as [`file::log::create_or_update::MissingCommitter`](crate::file::log::create_or_update::MissingCommitter).
32 pub fn commit<'a>(self, committer: impl Into<Option<gix_actor::SignatureRef<'a>>>) -> ExnResult<Vec<RefEdit>> {
33 self.commit_inner(committer.into())
34 }
35
36 /// Per-reference failures include [metadata](gix_error::Exn::metadata()) `reference` (bytes), the affected name.
37 fn commit_inner(self, committer: Option<gix_actor::SignatureRef<'_>>) -> ExnResult<Vec<RefEdit>> {
38 let mut updates = self.updates.expect("BUG: must call prepare before commit");
39 let delete_loose_refs = matches!(
40 self.packed_refs,
41 PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_)
42 );
43
44 // Perform updates first so live commits remain referenced
45 for change in &mut updates {
46 assert!(!change.update.deref, "Deref mode is turned into splits and turned off");
47 match &change.update.change {
48 // reflog first, then reference
49 Change::Update { log, new, expected } => {
50 let lock = change.lock.take();
51 let (update_ref, update_reflog) = match log.mode {
52 RefLog::Only => (false, true),
53 RefLog::AndReference => (true, true),
54 };
55 if update_reflog {
56 let log_update = match new {
57 Target::Symbolic(_) => {
58 // Special HACK: no reflog for symref changes as there is no OID involved which the reflog needs.
59 // Unless, the ref is new and we can obtain a peeled id
60 // identified by the expectation of what could be there, as is the case when cloning.
61 match expected {
62 PreviousValue::ExistingMustMatch(Target::Object(oid)) => {
63 Some((Some(gix_hash::ObjectId::null(oid.kind())), oid))
64 }
65 _ => None,
66 }
67 }
68 Target::Object(new_oid) => {
69 let previous = match expected {
70 // Here, this means that the ref already existed, and that it will receive (even transitively)
71 // the given value
72 PreviousValue::MustExistAndMatch(Target::Object(oid)) => Some(oid.to_owned()),
73 _ => None,
74 }
75 .or(change.leaf_referent_previous_oid);
76 Some((previous, new_oid))
77 }
78 };
79 if let Some((previous, new_oid)) = log_update {
80 let do_update = previous.as_ref() != Some(new_oid);
81 if do_update {
82 self.store
83 .reflog_create_or_append(
84 change.update.name.as_ref(),
85 previous,
86 new_oid,
87 committer,
88 log.message.as_ref(),
89 log.force_create_reflog,
90 )
91 .or_raise_erased(|| {
92 Message::new("Could not update reflog")
93 .with("reference", change.update.name.as_bstr())
94 })?;
95 }
96 }
97 }
98 // Don't do anything else while keeping the lock after potentially updating the reflog.
99 // We delay deletion of the reference and dropping the lock to after the packed-refs were
100 // safely written.
101 if delete_loose_refs && matches!(new, Target::Object(_)) {
102 change.lock = lock;
103 continue;
104 }
105 if update_ref && let Some(Err(err)) = lock.map(gix_lock::Marker::commit) {
106 // TODO: when Kind::IsADirectory becomes stable, use that.
107 let err = if err.instance.resource_path().is_dir() {
108 gix_tempfile::remove_dir::empty_depth_first(err.instance.resource_path())
109 .map_err(std::io::Error::other)
110 .and_then(|_| err.instance.commit().map_err(|err| err.error))
111 .err()
112 } else {
113 Some(err.error)
114 };
115
116 if let Some(err) = err {
117 return Err(err
118 .and_raise(Message::new("Could not commit reference").with("reference", change.name()))
119 .erased());
120 }
121 }
122 }
123 Change::Delete { .. } => {}
124 }
125 }
126
127 for change in &mut updates {
128 let (reflog_root, relative_name) = self.store.reflog_base_and_relative_path(change.update.name.as_ref());
129 match &change.update.change {
130 Change::Update { .. } => {}
131 Change::Delete { .. } => {
132 // Reflog deletion happens first in case it fails a ref without log is less terrible than
133 // a log without a reference.
134 let reflog_path = reflog_root.join(relative_name);
135 if let Err(err) = std::fs::remove_file(&reflog_path) {
136 if err.kind() != std::io::ErrorKind::NotFound {
137 return Err(err
138 .and_raise(Message::new("Could not delete reflog").with("reference", change.name()))
139 .erased());
140 }
141 } else {
142 gix_tempfile::remove_dir::empty_upward_until_boundary(
143 reflog_path.parent().expect("never without parent"),
144 &reflog_root,
145 )
146 .ok();
147 }
148 }
149 }
150 }
151
152 if let Some(t) = self.packed_transaction {
153 t.commit()
154 .or_raise_erased(|| message("Could not commit packed-ref transaction"))?;
155 // Always refresh ourselves right away to avoid races. We ignore errors as there may be many reasons this fails, and it's not
156 // critical to be done here. In other words, the pack may be refreshed at a later time and then it might work.
157 self.store.force_refresh_packed_buffer().ok();
158 }
159
160 for change in &mut updates {
161 let take_lock_and_delete = match &change.update.change {
162 Change::Update {
163 log: LogChange { mode, .. },
164 new,
165 ..
166 } => delete_loose_refs && *mode == RefLog::AndReference && matches!(new, Target::Object(_)),
167 Change::Delete { log: mode, .. } => *mode == RefLog::AndReference,
168 };
169 if take_lock_and_delete {
170 let lock = change.lock.take();
171 let reference_path = self.store.reference_path(change.update.name.as_ref());
172 if let Err(err) = std::fs::remove_file(reference_path)
173 && err.kind() != std::io::ErrorKind::NotFound
174 {
175 return Err(err
176 .and_raise(Message::new("Could not delete reference").with("reference", change.name()))
177 .erased());
178 }
179 drop(lock);
180 }
181 }
182 Ok(updates.into_iter().map(|edit| edit.update).collect())
183 }
184}
185
186use crate::transaction::PreviousValue;