Skip to main content

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;