git_ref/store/file/transaction/
commit.rs

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