Skip to main content

gix_ref/store/file/loose/
reflog.rs

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