Skip to main content

gix_ref/store/file/loose/
reflog.rs

1use gix_error::{ExnResult, Message, ResultExt, message};
2
3use std::{io::Read, path::PathBuf};
4
5use crate::{
6    FullNameRef,
7    store_impl::{file, file::log},
8};
9
10impl file::Store {
11    /// Returns true if a reflog exists for the given reference `name`.
12    ///
13    /// Please note that this method shouldn't be used to check if a log exists before trying to read it, but instead
14    /// is meant to be the fastest possible way to determine if a log exists or not.
15    /// If the caller needs to know if it's readable, try to read the log instead with a reverse or forward iterator.
16    pub fn reflog_exists<'a, Name, E>(&self, name: Name) -> Result<bool, E>
17    where
18        Name: TryInto<&'a FullNameRef, Error = E>,
19    {
20        Ok(self.reflog_path(name.try_into()?).is_file())
21    }
22
23    /// Return a reflog reverse iterator for the given fully qualified `name`, reading chunks from the back into the fixed buffer `buf`.
24    ///
25    /// The iterator will traverse log entries from most recent to oldest, reading the underlying file in chunks from the back.
26    /// Return `Ok(None)` if no reflog exists.
27    ///
28    /// Read failures include [metadata](gix_error::Exn::metadata()) `path` (native path), the resolved reflog path.
29    pub fn reflog_iter_rev<'a, 'b, Name, E>(
30        &self,
31        name: Name,
32        buf: &'b mut [u8],
33    ) -> ExnResult<Option<log::iter::Reverse<'b, std::fs::File>>>
34    where
35        Name: TryInto<&'a FullNameRef, Error = E>,
36        Result<&'a FullNameRef, E>: ResultExt<Success = &'a FullNameRef>,
37    {
38        let name = name
39            .try_into()
40            .or_raise_erased(|| message("The reflog name or path is not a valid ref name"))?;
41        self.reflog_iter_rev_inner(name, buf)
42            .or_raise_erased(|| read_reflog_error(self.reflog_path(name)))
43    }
44
45    pub(crate) fn reflog_iter_rev_inner<'b>(
46        &self,
47        name: &FullNameRef,
48        buf: &'b mut [u8],
49    ) -> std::io::Result<Option<log::iter::Reverse<'b, std::fs::File>>> {
50        let path = self.reflog_path(name);
51        if path.is_dir() {
52            return Ok(None);
53        }
54        match std::fs::File::open(&path) {
55            Ok(file) => Ok(Some(log::iter::reverse(file, buf)?)),
56            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
57            Err(err) => Err(err),
58        }
59    }
60
61    /// Return a reflog forward iterator for the given fully qualified `name` and write its file contents into `buf`.
62    ///
63    /// The iterator will traverse log entries from oldest to newest.
64    /// Return `Ok(None)` if no reflog exists.
65    ///
66    /// Read failures include [metadata](gix_error::Exn::metadata()) `path` (native path), the resolved reflog path.
67    pub fn reflog_iter<'a, 'b, Name, E>(
68        &self,
69        name: Name,
70        buf: &'b mut Vec<u8>,
71    ) -> ExnResult<Option<log::iter::Forward<'b>>>
72    where
73        Name: TryInto<&'a FullNameRef, Error = E>,
74        Result<&'a FullNameRef, E>: ResultExt<Success = &'a FullNameRef>,
75    {
76        let name = name
77            .try_into()
78            .or_raise_erased(|| message("The reflog name or path is not a valid ref name"))?;
79        self.reflog_iter_inner(name, buf)
80            .or_raise_erased(|| read_reflog_error(self.reflog_path(name)))
81    }
82
83    pub(crate) fn reflog_iter_inner<'b>(
84        &self,
85        name: &FullNameRef,
86        buf: &'b mut Vec<u8>,
87    ) -> std::io::Result<Option<log::iter::Forward<'b>>> {
88        let path = self.reflog_path(name);
89        match std::fs::File::open(&path) {
90            Ok(mut file) => {
91                buf.clear();
92                if let Err(err) = file.read_to_end(buf) {
93                    return if path.is_dir() { Ok(None) } else { Err(err) };
94                }
95                Ok(Some(log::iter::forward(buf)))
96            }
97            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
98            #[cfg(windows)]
99            Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => Ok(None),
100            Err(err) => Err(err),
101        }
102    }
103}
104
105impl file::Store {
106    /// Implements the logic required to transform a fully qualified refname into its log name
107    pub(crate) fn reflog_path(&self, name: &FullNameRef) -> PathBuf {
108        let (base, rela_path) = self.reflog_base_and_relative_path(name);
109        base.join(rela_path)
110    }
111}
112
113/// The raised error's [metadata](gix_error::Exn::metadata()) `path` (native path) identifies the resolved reflog path
114/// that could not be read.
115fn read_reflog_error(path: PathBuf) -> Message {
116    Message::new("Could not read reflog").with("path", path)
117}
118
119///
120pub mod create_or_update {
121    use std::{
122        borrow::Cow,
123        io::Write,
124        path::{Path, PathBuf},
125    };
126
127    use gix_error::{ErrorExt, ExnResult, Message, ResultExt};
128    use gix_hash::{ObjectId, oid};
129    use gix_object::bstr::BStr;
130
131    use crate::store_impl::{file, file::WriteReflog};
132
133    impl file::Store {
134        /// Append a reflog entry. Filesystem failures include [metadata](gix_error::Exn::metadata()) `path` (native
135        /// path), the affected file or directory.
136        /// A missing identity is reported as [`MissingCommitter`] only when a log entry must actually be written.
137        pub(crate) fn reflog_create_or_append(
138            &self,
139            name: &FullNameRef,
140            previous_oid: Option<ObjectId>,
141            new: &oid,
142            committer: Option<gix_actor::SignatureRef<'_>>,
143            message: &BStr,
144            mut force_create_reflog: bool,
145        ) -> ExnResult {
146            let (reflog_base, full_name) = self.reflog_base_and_relative_path(name);
147            match self.write_reflog {
148                WriteReflog::Normal | WriteReflog::Always => {
149                    if self.write_reflog == WriteReflog::Always {
150                        force_create_reflog = true;
151                    }
152                    let mut options = std::fs::OpenOptions::new();
153                    options.append(true).read(false);
154                    let log_path = reflog_base.join(&full_name);
155
156                    if force_create_reflog || self.should_autocreate_reflog(&full_name) {
157                        let parent_dir = log_path.parent().expect("always with parent directory");
158                        gix_tempfile::create_dir::all(parent_dir, Default::default()).or_raise_erased(|| {
159                            Message::new("Could not create reflog directory").with("path", parent_dir)
160                        })?;
161                        options.create(true);
162                    }
163
164                    let file_for_appending = match options.open(&log_path) {
165                        Ok(f) => Some(f),
166                        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
167                        Err(err) => {
168                            // TODO: when Kind::IsADirectory becomes stable, use that.
169                            if log_path.is_dir() {
170                                gix_tempfile::remove_dir::empty_depth_first(log_path.clone())
171                                    .and_then(|_| options.open(&log_path))
172                                    .map(Some)
173                                    .or_raise_erased(|| open_reflog_for_appending_error(log_path.as_path()))?
174                            } else {
175                                return Err(err.and_raise(open_reflog_for_appending_error(log_path)).erased());
176                            }
177                        }
178                    };
179
180                    if let Some(mut file) = file_for_appending {
181                        let committer = committer.ok_or_else(|| MissingCommitter.raise_erased())?;
182                        write!(file, "{} {} ", previous_oid.unwrap_or_else(|| new.kind().null()), new)
183                            .and_then(|_| committer.trim().write_to(&mut file))
184                            .and_then(|_| {
185                                if !message.is_empty() {
186                                    writeln!(file, "\t{message}")
187                                } else {
188                                    writeln!(file)
189                                }
190                            })
191                            .or_raise_erased(|| {
192                                Message::new("Could not append reflog entry").with("path", log_path.as_path())
193                            })?;
194                    }
195                    Ok(())
196                }
197                WriteReflog::Disable => Ok(()),
198            }
199        }
200
201        fn should_autocreate_reflog(&self, full_name: &Path) -> bool {
202            full_name.starts_with("refs/heads/")
203                || full_name.starts_with("refs/remotes/")
204                || full_name.starts_with("refs/notes/")
205                || full_name.starts_with("refs/worktree/") // NOTE: git does not write reflogs for worktree private refs
206                || full_name == Path::new("HEAD")
207        }
208
209        /// Returns the base paths for all reflogs
210        pub(in crate::store_impl::file) fn reflog_base_and_relative_path<'a>(
211            &self,
212            name: &'a FullNameRef,
213        ) -> (PathBuf, Cow<'a, Path>) {
214            let is_reflog = true;
215            let (base, name) = self.to_base_dir_and_relative_name(name, is_reflog);
216            (
217                base.join("logs"),
218                match &self.namespace {
219                    None => gix_path::to_native_path_on_windows(name.as_bstr()),
220                    Some(namespace) => gix_path::to_native_path_on_windows(
221                        namespace.to_owned().into_namespaced_name(name).into_inner(),
222                    ),
223                },
224            )
225        }
226    }
227
228    /// The raised error's [metadata](gix_error::Exn::metadata()) `path` (native path) identifies the reflog that could
229    /// not be opened for appending.
230    fn open_reflog_for_appending_error(path: impl Into<PathBuf>) -> Message {
231        Message::new("Could not open reflog for appending").with("path", path.into())
232    }
233
234    /// A reflog entry requires a committer identity which wasn't provided.
235    #[derive(Debug)]
236    pub struct MissingCommitter;
237
238    impl std::fmt::Display for MissingCommitter {
239        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240            f.write_str("reflog messages need a committer which isn't set")
241        }
242    }
243
244    impl std::error::Error for MissingCommitter {
245        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
246            Some(const { &gix_error::ClassificationMarker::VALIDATION })
247        }
248    }
249
250    #[cfg(test)]
251    mod tests;
252
253    use crate::FullNameRef;
254}