git-bug 0.2.4

A rust library for interfacing with git-bug repositories
Documentation
// git-bug-rs - A rust library for interfacing with git-bug repositories
//
// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of git-bug-rs/git-gub.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

//! A persistent [`Clock`] implementation.

use std::{
    fs::{File, OpenOptions},
    io::{Read, Write},
    path::{Path, PathBuf},
};

use super::{Clock, mem::MemClock};

/// A [`Clock`] implementation, that can be persisted to
/// disk, via the  [`from_path`][`PersistedClock::from_path`] or
/// [`save_to_file`][`PersistedClock::save_to_file`] functions.
#[derive(Debug)]
pub struct PersistedClock {
    clock: MemClock,
    file_path: PathBuf,
}

impl Clock for PersistedClock {
    type IncrementError = write::Error;
    type WitnessError = write::Error;

    fn time(&self) -> super::Time {
        self.clock.time()
    }

    fn increment(&mut self) -> Result<super::Time, Self::IncrementError> {
        let previous = self.clock.increment().expect("This error is infallible");
        self.save_to_file(false).map(|()| previous)
    }

    fn witness(&mut self, time: super::Time) -> Result<(), Self::WitnessError> {
        self.clock.witness(time).expect("The error is infallible");
        self.save_to_file(false)
    }
}

/// The Error returned by [`PersistedClock`].
pub mod write {
    #![allow(missing_docs)]
    use std::{io, num::ParseIntError, path::PathBuf};

    #[derive(Debug, thiserror::Error)]
    /// The Error returned by [`PersistedClock`][`super::PersistedClock`] write operations.
    pub enum Error {
        #[error("Failed to write this clock to {path}, because of {error}")]
        /// Failed to write clock
        Write { path: PathBuf, error: io::Error },

        #[error("Failed to open the path '{path}' for clock, because of {error}")]
        /// Failed to open the path to the clock
        Open { path: PathBuf, error: io::Error },

        #[error("Failed to read the contents of the path {path} for clock, because of {error}")]
        /// Failed to read to clock from file
        Read { path: PathBuf, error: io::Error },

        #[error(
            "Failed to parse the contents ('{contents}') of the path {path} as clock value, \
             because of {error}"
        )]
        /// Failed to parse the clock representation on disk
        Parse {
            path: PathBuf,
            error: ParseIntError,
            contents: String,
        },
    }
}

impl PersistedClock {
    fn open_file(path: &Path, options: &mut OpenOptions) -> Result<File, write::Error> {
        options.open(path).map_err(|err| write::Error::Open {
            error: err,
            path: path.to_owned(),
        })
    }

    /// Create a new [`PersistedClock`].
    /// This will override the contents of `path`.
    /// If you want to re-load a previously persisted clock, use
    /// [`PersistedClock::from_path`].
    ///
    /// # Errors
    /// If the `path` cannot be opened for writing.
    pub fn new(path: PathBuf) -> Result<Self, write::Error> {
        let mut me = Self {
            clock: MemClock::new(),
            file_path: path,
        };

        me.save_to_file(true)?;

        Ok(me)
    }

    /// Loads this clock from a previously persisted clock.
    ///
    /// # Errors
    /// If the `path` cannot be opened for reading and writing.
    pub fn from_path(path: PathBuf) -> Result<Self, write::Error> {
        let me = Self {
            clock: MemClock::new_with_value(Self::time_from_file(&path)?),
            file_path: path,
        };

        Ok(me)
    }

    fn time_from_file(path: &Path) -> Result<u64, write::Error> {
        let mut file = Self::open_file(path, OpenOptions::new().read(true))?;

        let mut contents = String::new();
        file.read_to_string(&mut contents)
            .map_err(|err| write::Error::Read {
                error: err,
                path: path.to_owned(),
            })?;

        let value: u64 = contents.parse().map_err(|err| write::Error::Parse {
            error: err,
            path: path.to_owned(),
            contents,
        })?;

        Ok(value)
    }

    /// Save this clock to disk.
    ///
    /// # Note
    /// The file needs to be opened in read-write mode, because we check that
    /// the file's content reflect our clock value.
    ///
    /// # Errors
    /// If the IO operations fail.
    ///
    /// # Panics
    /// If our time is behind the files stored time.
    pub fn save_to_file(&mut self, create: bool) -> Result<(), write::Error> {
        if self.file_path.exists() {
            assert!(
                Self::time_from_file(&self.file_path)? <= self.time().0,
                "The time should have not been changed under our feet."
            );
        }

        let mut file = Self::open_file(
            &self.file_path,
            OpenOptions::new().write(true).create(create),
        )?;

        write!(file, "{}", self.time().0).map_err(|err| write::Error::Write {
            error: err,
            path: self.file_path.clone(),
        })
    }
}