From 31f4de71d5a3f203e00de80758eea233133528ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:43:03 +0000 Subject: [PATCH 1/8] Initial plan From 11f58e29914cbd8ea7d15c9d57aa66c255e6ea8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:59:23 +0000 Subject: [PATCH 2/8] Convert from time library to jiff library Co-authored-by: sourcefrog <346355+sourcefrog@users.noreply.github.com> --- Cargo.lock | 59 +++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 9 ++----- src/band.rs | 18 ++++++------- src/bin/conserve.rs | 14 +++++----- src/change.rs | 4 +-- src/entry.rs | 4 +-- src/index/entry.rs | 10 +++---- src/restore.rs | 6 ++--- src/show.rs | 26 +++++++++--------- src/source.rs | 3 ++- src/source/entry.rs | 6 ++--- src/transport.rs | 4 +-- src/transport/local.rs | 7 +++-- src/transport/s3.rs | 3 ++- src/transport/sftp.rs | 4 +-- src/unix_time.rs | 49 +++++++++++++++++++++++++--------- tests/old_archives.rs | 28 +++++++++---------- tests/s3_integration.rs | 13 +++++---- 18 files changed, 172 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65771f90..d3c3f188 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,6 +811,7 @@ dependencies = [ "hex", "indoc", "itertools", + "jiff", "lazy_static", "libssh2-sys", "lru 0.16.3", @@ -834,7 +835,6 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "thousands", - "time", "tokio", "tracing", "tracing-appender", @@ -1029,7 +1029,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde_core", ] [[package]] @@ -1840,6 +1839,47 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-static" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2193,6 +2233,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 8ec94617..063416f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,12 +61,7 @@ strum_macros = "0.26" tempfile = "3" thiserror = "2.0" thousands = "0.2.0" -time = { version = "0.3.47", features = [ - "local-offset", - "macros", - "serde", - "serde-human-readable", -] } +jiff = { version = "0.2.19", features = ["serde"] } tokio = { version = "1.43", features = ["full", "test-util", "tracing"] } tracing = "0.1" tracing-appender = "0.2" @@ -93,7 +88,7 @@ version = "0.1.5" [dependencies.tracing-subscriber] version = "0.3.20" -features = ["env-filter", "fmt", "json", "local-time", "time"] +features = ["env-filter", "fmt", "json", "local-time"] [dev-dependencies] assert_cmd = "2.1.2" diff --git a/src/band.rs b/src/band.rs index 9a0ab5ec..57b32e77 100644 --- a/src/band.rs +++ b/src/band.rs @@ -27,7 +27,7 @@ use std::sync::Arc; use crate::transport::Transport; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; +use jiff::Timestamp; use tracing::{debug, trace, warn}; use crate::jsonio::{read_json, write_json}; @@ -113,10 +113,10 @@ pub struct Info { pub is_closed: bool, /// Time Conserve started writing this band. - pub start_time: OffsetDateTime, + pub start_time: Timestamp, /// Time this band was completed, if it is complete. - pub end_time: Option, + pub end_time: Option, /// Number of hunks present in the index, if that is known. pub index_hunk_count: Option, @@ -153,7 +153,7 @@ impl Band { Some("23.2.0".to_owned()) }; let head = Head { - start_time: OffsetDateTime::now_utc().unix_timestamp(), + start_time: Timestamp::now().as_second(), band_format_version, format_flags: format_flags.into(), }; @@ -171,7 +171,7 @@ impl Band { &self.transport, BAND_TAIL_FILENAME, &Tail { - end_time: OffsetDateTime::now_utc().unix_timestamp(), + end_time: Timestamp::now().as_second(), index_hunk_count: Some(index_hunk_count), }, ) @@ -267,7 +267,7 @@ impl Band { pub async fn get_info(&self) -> Result { let tail_option: Option = read_json(&self.transport, BAND_TAIL_FILENAME).await?; let start_time = - OffsetDateTime::from_unix_timestamp(self.head.start_time).map_err(|_| { + Timestamp::from_second(self.head.start_time).map_err(|_| { Error::InvalidMetadata { details: format!("Invalid band start timestamp {:?}", self.head.start_time), } @@ -275,7 +275,7 @@ impl Band { let end_time = tail_option .as_ref() .map(|tail| { - OffsetDateTime::from_unix_timestamp(tail.end_time).map_err(|_| { + Timestamp::from_second(tail.end_time).map_err(|_| { Error::InvalidMetadata { details: format!("Invalid band end timestamp {:?}", tail.end_time), } @@ -352,10 +352,10 @@ mod tests { assert_eq!(info.id.to_string(), "b0000"); assert!(info.is_closed); assert_eq!(info.index_hunk_count, Some(0)); - let dur = info.end_time.expect("info has an end_time") - info.start_time; + let dur = info.end_time.expect("info has an end_time").since(info.start_time).unwrap(); // Test should have taken (much) less than 5s between starting and finishing // the band. (It might fail if you set a breakpoint right there.) - assert!(dur < Duration::from_secs(5)); + assert!(dur.total(jiff::Unit::Second).unwrap() < 5.0); } #[tokio::test] diff --git a/src/bin/conserve.rs b/src/bin/conserve.rs index ce5cdd7f..684d35a2 100644 --- a/src/bin/conserve.rs +++ b/src/bin/conserve.rs @@ -23,7 +23,6 @@ use std::time::Instant; use clap::builder::{Styles, styling}; use clap::{Parser, Subcommand}; use conserve::change::Change; -use time::UtcOffset; #[allow(unused_imports)] use tracing::{Level, debug, error, info, trace, warn}; @@ -31,9 +30,9 @@ use crate::transport::Transport; use conserve::termui::{TermUiMonitor, TraceTimeStyle, enable_tracing}; use conserve::*; -/// Local timezone offset, calculated once at startup, to avoid issues about +/// Local timezone, calculated once at startup, to avoid issues about /// looking at the environment once multiple threads are running. -static LOCAL_OFFSET: RwLock = RwLock::new(UtcOffset::UTC); +static LOCAL_TZ: RwLock = RwLock::new(jiff::tz::TimeZone::UTC); #[mutants::skip] // only visual effects, not worth testing fn clap_styles() -> Styles { @@ -633,7 +632,7 @@ impl Command { let timezone = if *utc { None } else { - Some(*LOCAL_OFFSET.read().unwrap()) + Some(LOCAL_TZ.read().unwrap().clone()) }; let archive = Archive::open(Transport::new(archive).await?).await?; let options = ShowVersionsOptions { @@ -716,10 +715,9 @@ fn make_change_callback( } fn main() -> Result { - // Before anything else, get the local time offset, to avoid `time-rs` - // problems with loading it when threads are running. - *LOCAL_OFFSET.write().unwrap() = - UtcOffset::current_local_offset().expect("get local time offset"); + // Before anything else, get the local timezone, to avoid issues + // with loading it when threads are running. + *LOCAL_TZ.write().unwrap() = jiff::tz::TimeZone::system(); let args = Args::parse(); let start_time = Instant::now(); let console_level = if args.debug { diff --git a/src/change.rs b/src/change.rs index bc6af0af..c3de4ff7 100644 --- a/src/change.rs +++ b/src/change.rs @@ -16,7 +16,7 @@ use std::fmt; use serde::Serialize; -use time::OffsetDateTime; +use jiff::Timestamp; use crate::{Apath, EntryTrait, Kind, Owner, Result, UnixMode}; @@ -138,7 +138,7 @@ pub struct EntryMetadata { // TODO: Eventually unify with EntryValue or Entry? #[serde(flatten)] pub kind: KindMetadata, - pub mtime: OffsetDateTime, + pub mtime: Timestamp, #[serde(flatten)] pub owner: Owner, pub unix_mode: UnixMode, diff --git a/src/entry.rs b/src/entry.rs index 24afd604..2ba85785 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -17,7 +17,7 @@ use std::fmt::Debug; use serde::Serialize; -use time::OffsetDateTime; +use jiff::Timestamp; use crate::*; @@ -29,7 +29,7 @@ use crate::*; pub trait EntryTrait: Debug { fn apath(&self) -> &Apath; fn kind(&self) -> Kind; - fn mtime(&self) -> OffsetDateTime; + fn mtime(&self) -> Timestamp; fn size(&self) -> Option; fn symlink_target(&self) -> Option<&str>; fn unix_mode(&self) -> UnixMode; diff --git a/src/index/entry.rs b/src/index/entry.rs index f2551752..b4456697 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -12,7 +12,7 @@ // GNU General Public License for more details. use serde_json::json; -use time::OffsetDateTime; +use jiff::Timestamp; use crate::apath::Apath; use crate::entry::EntryTrait; @@ -83,8 +83,8 @@ impl EntryTrait for IndexEntry { } #[inline] - fn mtime(&self) -> OffsetDateTime { - OffsetDateTime::from_unix_seconds_and_nanos(self.mtime, self.mtime_nanos) + fn mtime(&self) -> Timestamp { + Timestamp::from_unix_seconds_and_nanos(self.mtime, self.mtime_nanos) } /// Size of the file, if it is a file. None for directories and symlinks. @@ -142,8 +142,8 @@ impl IndexEntry { kind: source.kind(), addrs: Vec::new(), target: source.symlink_target().map(|t| t.to_owned()), - mtime: mtime.unix_timestamp(), - mtime_nanos: mtime.nanosecond(), + mtime: mtime.as_second(), + mtime_nanos: mtime.subsec_nanosecond() as u32, unix_mode: source.unix_mode(), owner: source.owner().to_owned(), } diff --git a/src/restore.rs b/src/restore.rs index fbbc717d..4ac74219 100644 --- a/src/restore.rs +++ b/src/restore.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use filetime::set_file_handle_times; #[cfg(unix)] use filetime::set_symlink_file_times; -use time::OffsetDateTime; +use jiff::Timestamp; use tracing::{instrument, trace}; use crate::blockdir::BlockDir; @@ -176,7 +176,7 @@ fn restore_dir(apath: &Apath, restore_path: &Path, options: &RestoreOptions) -> struct DirDeferral { path: PathBuf, unix_mode: UnixMode, - mtime: OffsetDateTime, + mtime: Timestamp, owner: Owner, } @@ -200,7 +200,7 @@ fn apply_deferrals(deferrals: &[DirDeferral], monitor: Arc) -> Resu source, }); } - if let Err(source) = filetime::set_file_mtime(path, (*mtime).to_file_time()) { + if let Err(source) = filetime::set_file_mtime(path, mtime.to_file_time()) { monitor.error(Error::RestoreModificationTime { path: path.clone(), source, diff --git a/src/show.rs b/src/show.rs index 650fdcd2..ae947be7 100644 --- a/src/show.rs +++ b/src/show.rs @@ -20,8 +20,6 @@ use std::borrow::Cow; use std::io::{BufWriter, Write}; use std::sync::Arc; -use time::UtcOffset; -use time::format_description::well_known::Rfc3339; use tracing::error; use crate::index::entry::IndexEntry; @@ -42,7 +40,7 @@ pub struct ShowVersionsOptions { /// Show how much time the backup took, or "incomplete" if it never finished. pub backup_duration: bool, /// Show times in this zone. - pub timezone: Option, + pub timezone: Option, } /// Print a list of versions, one per line, on stdout. @@ -78,24 +76,28 @@ pub async fn show_versions( }; if options.start_time { - let mut start_time = info.start_time; - if let Some(timezone) = options.timezone { - start_time = start_time.to_offset(timezone); - } + let start_time_str = if let Some(timezone) = options.timezone.as_ref() { + info.start_time.to_zoned(timezone.clone()).to_string() + } else { + info.start_time.to_string() + }; l.push(format!( "{date:<25}", // "yyyy-mm-ddThh:mm:ss+oooo" => 25 - date = start_time.format(&Rfc3339).unwrap(), + date = start_time_str, )); } if options.backup_duration { let duration_str: Cow = if info.is_closed { if let Some(end_time) = info.end_time { - let duration = end_time - info.start_time; - if let Ok(duration) = duration.try_into() { - duration_to_hms(duration).into() - } else { + let span = end_time.since(info.start_time).unwrap(); + // Convert jiff::Span to std::time::Duration + let total_nanos = span.total(jiff::Unit::Nanosecond).unwrap_or(0.0); + if total_nanos < 0.0 { Cow::Borrowed("negative") + } else { + let duration = std::time::Duration::from_nanos(total_nanos as u64); + duration_to_hms(duration).into() } } else { Cow::Borrowed("unknown") diff --git a/src/source.rs b/src/source.rs index 68533b20..df3c6029 100644 --- a/src/source.rs +++ b/src/source.rs @@ -30,6 +30,7 @@ use crate::entry::KindMeta; use crate::monitor::Monitor; use crate::stats::SourceIterStats; use crate::tree::TreeSize; +use crate::unix_time::ToTimestamp; use crate::*; /// A real tree on the filesystem, as a backup source. @@ -95,7 +96,7 @@ fn entry_from_fs_metadata( let mtime = metadata .modified() .expect("Failed to get file mtime") - .into(); + .to_timestamp(); let kind_meta = if metadata.is_file() { KindMeta::File { size: metadata.len(), diff --git a/src/source/entry.rs b/src/source/entry.rs index 3b709152..2ac9d28e 100644 --- a/src/source/entry.rs +++ b/src/source/entry.rs @@ -1,5 +1,5 @@ use serde::{self, Serialize}; -use time::OffsetDateTime; +use jiff::Timestamp; use crate::{Apath, EntryTrait, Kind, Owner, UnixMode, entry::KindMeta}; @@ -13,7 +13,7 @@ pub struct Entry { pub(crate) kind_meta: KindMeta, /// Modification time. - pub(crate) mtime: OffsetDateTime, + pub(crate) mtime: Timestamp, pub(crate) unix_mode: UnixMode, #[serde(flatten)] pub(crate) owner: Owner, @@ -28,7 +28,7 @@ impl EntryTrait for Entry { Kind::from(&self.kind_meta) } - fn mtime(&self) -> OffsetDateTime { + fn mtime(&self) -> Timestamp { self.mtime } diff --git a/src/transport.rs b/src/transport.rs index 224cd520..47f4c177 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -19,7 +19,7 @@ use std::sync::{Arc, Mutex}; use std::{fmt, result}; use bytes::Bytes; -use time::OffsetDateTime; +use jiff::Timestamp; use url::Url; use crate::*; @@ -307,7 +307,7 @@ pub struct Metadata { pub kind: Kind, /// Last modified time. - pub modified: OffsetDateTime, + pub modified: Timestamp, } impl Metadata { diff --git a/src/transport/local.rs b/src/transport/local.rs index d03f5299..d43fcef2 100644 --- a/src/transport/local.rs +++ b/src/transport/local.rs @@ -24,6 +24,7 @@ use tracing::{error, trace, warn}; use url::Url; use crate::Kind; +use crate::unix_time::ToTimestamp; use super::{DirEntry, Error, Metadata, Result, WriteMode}; @@ -146,7 +147,7 @@ impl super::Protocol for Protocol { let modified = fsmeta .modified() .map_err(|err| Error::io_error(&path, err))? - .into(); + .to_timestamp(); Ok(Metadata { len: fsmeta.len(), kind: fsmeta.file_type().into(), @@ -212,12 +213,10 @@ async fn collect_tokio_dir_entry(dir_entry: tokio::fs::DirEntry) -> Option OffsetDateTime::now_utc()); + assert!(metadata.modified.checked_add(jiff::Span::new().seconds(60)).unwrap() > jiff::Timestamp::now()); assert!( transport .metadata("nopoem") diff --git a/src/transport/s3.rs b/src/transport/s3.rs index 8d2960fc..71682958 100644 --- a/src/transport/s3.rs +++ b/src/transport/s3.rs @@ -46,6 +46,7 @@ use bytes::Bytes; use tracing::{debug, error, trace}; use url::Url; +use crate::unix_time::ToTimestamp; use super::{DirEntry, Error, ErrorKind, Kind, Metadata, Result, WriteMode}; pub(super) struct Protocol { @@ -341,7 +342,7 @@ impl super::Protocol for Protocol { Ok(Metadata { kind: Kind::File, len, - modified: modified.into(), + modified: modified.to_timestamp(), }) } Err(err) => { diff --git a/src/transport/sftp.rs b/src/transport/sftp.rs index 4ffa34ee..5d78a559 100644 --- a/src/transport/sftp.rs +++ b/src/transport/sftp.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use async_trait::async_trait; use bytes::Bytes; use ssh2::Sftp; -use time::OffsetDateTime; +use jiff::Timestamp; use tokio::task::spawn_blocking; use tracing::{error, info, trace, warn}; use url::Url; @@ -208,7 +208,7 @@ impl super::Protocol for Protocol { url: Some(self.join_url(relpath)), } })?; - let modified = OffsetDateTime::from_unix_timestamp(modified as i64).map_err(|err| { + let modified = Timestamp::from_second(modified as i64).map_err(|err| { warn!("Invalid mtime for {full_path:?}"); super::Error { kind: ErrorKind::Other, diff --git a/src/unix_time.rs b/src/unix_time.rs index 8f172562..ebb8ca1c 100644 --- a/src/unix_time.rs +++ b/src/unix_time.rs @@ -13,32 +13,57 @@ //! Times relative to the Unix epoch. //! -//! In particular, glue between [filetime] and [time]. +//! In particular, glue between [filetime] and [jiff]. use filetime::FileTime; -use time::OffsetDateTime; +use jiff::Timestamp; +use std::time::SystemTime; pub(crate) trait FromUnixAndNanos { fn from_unix_seconds_and_nanos(unix_seconds: i64, nanoseconds: u32) -> Self; } -impl FromUnixAndNanos for OffsetDateTime { +impl FromUnixAndNanos for Timestamp { fn from_unix_seconds_and_nanos(unix_seconds: i64, nanoseconds: u32) -> Self { - OffsetDateTime::from_unix_timestamp(unix_seconds) + Timestamp::from_second(unix_seconds) .unwrap() - .replace_nanosecond(nanoseconds) + .checked_add(jiff::Span::new().nanoseconds(nanoseconds as i64)) .unwrap() } } #[allow(unused)] // really unused at present, but might be useful -pub(crate) trait ToOffsetDateTime { - fn to_offset_date_time(&self) -> OffsetDateTime; +pub(crate) trait ToTimestamp { + fn to_timestamp(&self) -> Timestamp; } -impl ToOffsetDateTime for FileTime { - fn to_offset_date_time(&self) -> OffsetDateTime { - OffsetDateTime::from_unix_seconds_and_nanos(self.unix_seconds(), self.nanoseconds()) +impl ToTimestamp for FileTime { + fn to_timestamp(&self) -> Timestamp { + Timestamp::from_unix_seconds_and_nanos(self.unix_seconds(), self.nanoseconds()) + } +} + +impl ToTimestamp for SystemTime { + fn to_timestamp(&self) -> Timestamp { + match self.duration_since(SystemTime::UNIX_EPOCH) { + Ok(dur) => { + let secs = dur.as_secs() as i64; + let nanos = dur.subsec_nanos(); + Timestamp::from_unix_seconds_and_nanos(secs, nanos) + } + Err(e) => { + // Time is before Unix epoch + let dur = e.duration(); + let secs = -(dur.as_secs() as i64); + let nanos = dur.subsec_nanos(); + if nanos == 0 { + Timestamp::from_unix_seconds_and_nanos(secs, 0) + } else { + // Need to adjust for the fractional part + Timestamp::from_unix_seconds_and_nanos(secs - 1, 1_000_000_000 - nanos) + } + } + } } } @@ -46,8 +71,8 @@ pub(crate) trait ToFileTime { fn to_file_time(&self) -> FileTime; } -impl ToFileTime for OffsetDateTime { +impl ToFileTime for Timestamp { fn to_file_time(&self) -> FileTime { - FileTime::from_unix_time(self.unix_timestamp(), self.nanosecond()) + FileTime::from_unix_time(self.as_second(), self.subsec_nanosecond() as u32) } } diff --git a/tests/old_archives.rs b/tests/old_archives.rs index 45a3da1c..72128fa4 100644 --- a/tests/old_archives.rs +++ b/tests/old_archives.rs @@ -26,7 +26,7 @@ use predicates::prelude::*; use pretty_assertions::assert_eq; use conserve::*; -use time::OffsetDateTime; +use conserve::unix_time::ToTimestamp; mod util; use util::{copy_testdata_archive, testdata_archive_path}; @@ -167,25 +167,23 @@ async fn restore_old_archive() { // Check that mtimes are restored. The sub-second times are not tested // because their behavior might vary depending on the local filesystem. - let file_mtime = OffsetDateTime::from( - metadata(dest.child("hello").path()) - .unwrap() - .modified() - .unwrap(), - ); + let file_mtime = metadata(dest.child("hello").path()) + .unwrap() + .modified() + .unwrap() + .to_timestamp(); assert_eq!( - file_mtime.unix_timestamp(), + file_mtime.as_second(), 1592266523, "mtime not restored correctly" ); - let dir_mtime = OffsetDateTime::from( - metadata(dest.child("subdir").path()) - .unwrap() - .modified() - .unwrap(), - ); - assert_eq!(dir_mtime.unix_timestamp(), 1592266523); + let dir_mtime = metadata(dest.child("subdir").path()) + .unwrap() + .modified() + .unwrap() + .to_timestamp(); + assert_eq!(dir_mtime.as_second(), 1592266523); } } diff --git a/tests/s3_integration.rs b/tests/s3_integration.rs index 9a6b3520..41d62cf0 100644 --- a/tests/s3_integration.rs +++ b/tests/s3_integration.rs @@ -29,8 +29,7 @@ use aws_sdk_s3::types::{ }; use indoc::indoc; use rand::Rng; -use time::OffsetDateTime; -use time::macros::format_description; +use jiff::Timestamp; use tokio::runtime::Runtime; struct TempBucket { @@ -52,9 +51,13 @@ impl TempBucket { let mut rng = rand::rng(); let bucket_name = format!( "conserve-s3-integration-{time}-{rand:x}", - time = OffsetDateTime::now_utc() - .format(format_description!("[year][month][day]-[hour][minute]")) - .expect("Format time"), + time = Timestamp::now() + .to_string() + .chars() + .filter(|c| c.is_alphanumeric() || *c == '-') + .collect::() + .replace("T", "-") + .split_at(16).0.to_owned(), // Take first 16 chars: "YYYY-MM-DD-HHMM" rand = rng.random::() ); let app_name = AppName::new(format!( From f600a92fa065f0e53aa5e1b557d3788823dabe08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:00:40 +0000 Subject: [PATCH 3/8] Make ToTimestamp trait public for tests Co-authored-by: sourcefrog <346355+sourcefrog@users.noreply.github.com> --- src/unix_time.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unix_time.rs b/src/unix_time.rs index ebb8ca1c..49ab9b57 100644 --- a/src/unix_time.rs +++ b/src/unix_time.rs @@ -33,7 +33,7 @@ impl FromUnixAndNanos for Timestamp { } #[allow(unused)] // really unused at present, but might be useful -pub(crate) trait ToTimestamp { +pub trait ToTimestamp { fn to_timestamp(&self) -> Timestamp; } From 551996680c2aa59e9bdf0a1d1fec530e3acdb61a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:05:09 +0000 Subject: [PATCH 4/8] Address code review feedback: fix unsafe split_at and redundant check Co-authored-by: sourcefrog <346355+sourcefrog@users.noreply.github.com> --- src/show.rs | 12 ++++++------ tests/s3_integration.rs | 5 ++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/show.rs b/src/show.rs index ae947be7..bb935a83 100644 --- a/src/show.rs +++ b/src/show.rs @@ -92,12 +92,12 @@ pub async fn show_versions( if let Some(end_time) = info.end_time { let span = end_time.since(info.start_time).unwrap(); // Convert jiff::Span to std::time::Duration - let total_nanos = span.total(jiff::Unit::Nanosecond).unwrap_or(0.0); - if total_nanos < 0.0 { - Cow::Borrowed("negative") - } else { - let duration = std::time::Duration::from_nanos(total_nanos as u64); - duration_to_hms(duration).into() + match span.total(jiff::Unit::Nanosecond) { + Ok(total_nanos) if total_nanos >= 0.0 => { + let duration = std::time::Duration::from_nanos(total_nanos as u64); + duration_to_hms(duration).into() + } + _ => Cow::Borrowed("negative"), } } else { Cow::Borrowed("unknown") diff --git a/tests/s3_integration.rs b/tests/s3_integration.rs index 41d62cf0..1c273496 100644 --- a/tests/s3_integration.rs +++ b/tests/s3_integration.rs @@ -55,9 +55,8 @@ impl TempBucket { .to_string() .chars() .filter(|c| c.is_alphanumeric() || *c == '-') - .collect::() - .replace("T", "-") - .split_at(16).0.to_owned(), // Take first 16 chars: "YYYY-MM-DD-HHMM" + .take(16) // Take first 16 characters + .collect::(), rand = rng.random::() ); let app_name = AppName::new(format!( From 26345ae6cb980ad03420a91ed3c308fa91fda0cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:38:59 +0000 Subject: [PATCH 5/8] Simplify unix_time.rs by removing unnecessary traits and using jiff's built-in conversions Co-authored-by: sourcefrog <346355+sourcefrog@users.noreply.github.com> --- src/index/entry.rs | 4 +-- src/restore.rs | 8 +++--- src/source.rs | 8 ++---- src/transport/local.rs | 17 +++++++---- src/transport/s3.rs | 5 ++-- src/unix_time.rs | 64 ++++++------------------------------------ tests/old_archives.rs | 26 +++++++++-------- 7 files changed, 48 insertions(+), 84 deletions(-) diff --git a/src/index/entry.rs b/src/index/entry.rs index b4456697..45eb0568 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -19,7 +19,7 @@ use crate::entry::EntryTrait; use crate::kind::Kind; use crate::owner::Owner; use crate::unix_mode::UnixMode; -use crate::unix_time::FromUnixAndNanos; +use crate::unix_time::timestamp_from_unix_nanos; use crate::{blockdir, source}; /// Description of one archived file. @@ -84,7 +84,7 @@ impl EntryTrait for IndexEntry { #[inline] fn mtime(&self) -> Timestamp { - Timestamp::from_unix_seconds_and_nanos(self.mtime, self.mtime_nanos) + timestamp_from_unix_nanos(self.mtime, self.mtime_nanos) } /// Size of the file, if it is a file. None for directories and symlinks. diff --git a/src/restore.rs b/src/restore.rs index 4ac74219..748d0993 100644 --- a/src/restore.rs +++ b/src/restore.rs @@ -29,7 +29,7 @@ use crate::counters::Counter; use crate::index::entry::IndexEntry; use crate::io::{directory_is_empty, ensure_dir_exists}; use crate::monitor::Monitor; -use crate::unix_time::ToFileTime; +use crate::unix_time::timestamp_to_file_time; use crate::*; /// Description of how to restore a tree. @@ -200,7 +200,7 @@ fn apply_deferrals(deferrals: &[DirDeferral], monitor: Arc) -> Resu source, }); } - if let Err(source) = filetime::set_file_mtime(path, mtime.to_file_time()) { + if let Err(source) = filetime::set_file_mtime(path, timestamp_to_file_time(mtime)) { monitor.error(Error::RestoreModificationTime { path: path.clone(), source, @@ -247,7 +247,7 @@ async fn restore_file( source, })?; - let mtime = Some(source_entry.mtime().to_file_time()); + let mtime = Some(timestamp_to_file_time(&source_entry.mtime())); set_file_handle_times(&out, mtime, mtime).map_err(|source| Error::RestoreModificationTime { path: path.clone(), source, @@ -291,7 +291,7 @@ fn restore_symlink(path: &Path, entry: &IndexEntry) -> Result<()> { source, }); } - let mtime = entry.mtime().to_file_time(); + let mtime = timestamp_to_file_time(&entry.mtime()); if let Err(source) = set_symlink_file_times(path, mtime, mtime) { return Err(Error::RestoreModificationTime { path: path.to_owned(), diff --git a/src/source.rs b/src/source.rs index df3c6029..37c6aa8c 100644 --- a/src/source.rs +++ b/src/source.rs @@ -23,6 +23,7 @@ use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::sync::Arc; +use jiff::Timestamp; use tracing::{error, warn}; use crate::counters::Counter; @@ -30,7 +31,6 @@ use crate::entry::KindMeta; use crate::monitor::Monitor; use crate::stats::SourceIterStats; use crate::tree::TreeSize; -use crate::unix_time::ToTimestamp; use crate::*; /// A real tree on the filesystem, as a backup source. @@ -93,10 +93,8 @@ fn entry_from_fs_metadata( source_path: &Path, metadata: &fs::Metadata, ) -> Result { - let mtime = metadata - .modified() - .expect("Failed to get file mtime") - .to_timestamp(); + let mtime = Timestamp::try_from(metadata.modified().expect("Failed to get file mtime")) + .expect("File mtime converts to Timestamp"); let kind_meta = if metadata.is_file() { KindMeta::File { size: metadata.len(), diff --git a/src/transport/local.rs b/src/transport/local.rs index d43fcef2..25f0ea16 100644 --- a/src/transport/local.rs +++ b/src/transport/local.rs @@ -18,13 +18,13 @@ use std::{io, path}; use async_trait::async_trait; use bytes::Bytes; +use jiff::Timestamp; use tempfile::TempDir; use tokio::sync::Semaphore; use tracing::{error, trace, warn}; use url::Url; use crate::Kind; -use crate::unix_time::ToTimestamp; use super::{DirEntry, Error, Metadata, Result, WriteMode}; @@ -144,10 +144,17 @@ impl super::Protocol for Protocol { let fsmeta = tokio::fs::metadata(&path) .await .map_err(|err| Error::io_error(&path, err))?; - let modified = fsmeta - .modified() - .map_err(|err| Error::io_error(&path, err))? - .to_timestamp(); + let modified = Timestamp::try_from( + fsmeta + .modified() + .map_err(|err| Error::io_error(&path, err))?, + ) + .map_err(|err| { + Error::io_error( + &path, + std::io::Error::new(std::io::ErrorKind::InvalidData, err), + ) + })?; Ok(Metadata { len: fsmeta.len(), kind: fsmeta.file_type().into(), diff --git a/src/transport/s3.rs b/src/transport/s3.rs index 71682958..a9786658 100644 --- a/src/transport/s3.rs +++ b/src/transport/s3.rs @@ -43,10 +43,10 @@ use aws_types::SdkConfig; use aws_types::region::Region; use base64::Engine; use bytes::Bytes; +use jiff::Timestamp; use tracing::{debug, error, trace}; use url::Url; -use crate::unix_time::ToTimestamp; use super::{DirEntry, Error, ErrorKind, Kind, Metadata, Result, WriteMode}; pub(super) struct Protocol { @@ -342,7 +342,8 @@ impl super::Protocol for Protocol { Ok(Metadata { kind: Kind::File, len, - modified: modified.to_timestamp(), + modified: Timestamp::try_from(modified) + .expect("S3 last_modified converts to Timestamp"), }) } Err(err) => { diff --git a/src/unix_time.rs b/src/unix_time.rs index 49ab9b57..58b34cae 100644 --- a/src/unix_time.rs +++ b/src/unix_time.rs @@ -17,62 +17,16 @@ use filetime::FileTime; use jiff::Timestamp; -use std::time::SystemTime; -pub(crate) trait FromUnixAndNanos { - fn from_unix_seconds_and_nanos(unix_seconds: i64, nanoseconds: u32) -> Self; +/// Helper to construct a Timestamp from Unix seconds and nanoseconds. +pub(crate) fn timestamp_from_unix_nanos(unix_seconds: i64, nanoseconds: u32) -> Timestamp { + Timestamp::from_second(unix_seconds) + .unwrap() + .checked_add(jiff::Span::new().nanoseconds(nanoseconds as i64)) + .unwrap() } -impl FromUnixAndNanos for Timestamp { - fn from_unix_seconds_and_nanos(unix_seconds: i64, nanoseconds: u32) -> Self { - Timestamp::from_second(unix_seconds) - .unwrap() - .checked_add(jiff::Span::new().nanoseconds(nanoseconds as i64)) - .unwrap() - } -} - -#[allow(unused)] // really unused at present, but might be useful -pub trait ToTimestamp { - fn to_timestamp(&self) -> Timestamp; -} - -impl ToTimestamp for FileTime { - fn to_timestamp(&self) -> Timestamp { - Timestamp::from_unix_seconds_and_nanos(self.unix_seconds(), self.nanoseconds()) - } -} - -impl ToTimestamp for SystemTime { - fn to_timestamp(&self) -> Timestamp { - match self.duration_since(SystemTime::UNIX_EPOCH) { - Ok(dur) => { - let secs = dur.as_secs() as i64; - let nanos = dur.subsec_nanos(); - Timestamp::from_unix_seconds_and_nanos(secs, nanos) - } - Err(e) => { - // Time is before Unix epoch - let dur = e.duration(); - let secs = -(dur.as_secs() as i64); - let nanos = dur.subsec_nanos(); - if nanos == 0 { - Timestamp::from_unix_seconds_and_nanos(secs, 0) - } else { - // Need to adjust for the fractional part - Timestamp::from_unix_seconds_and_nanos(secs - 1, 1_000_000_000 - nanos) - } - } - } - } -} - -pub(crate) trait ToFileTime { - fn to_file_time(&self) -> FileTime; -} - -impl ToFileTime for Timestamp { - fn to_file_time(&self) -> FileTime { - FileTime::from_unix_time(self.as_second(), self.subsec_nanosecond() as u32) - } +/// Helper to convert a Timestamp to a FileTime. +pub(crate) fn timestamp_to_file_time(timestamp: &Timestamp) -> FileTime { + FileTime::from_unix_time(timestamp.as_second(), timestamp.subsec_nanosecond() as u32) } diff --git a/tests/old_archives.rs b/tests/old_archives.rs index 72128fa4..5ab748e8 100644 --- a/tests/old_archives.rs +++ b/tests/old_archives.rs @@ -22,11 +22,11 @@ use assert_fs::TempDir; use assert_fs::prelude::*; use conserve::counters::Counter; use conserve::monitor::test::TestMonitor; +use jiff::Timestamp; use predicates::prelude::*; use pretty_assertions::assert_eq; use conserve::*; -use conserve::unix_time::ToTimestamp; mod util; use util::{copy_testdata_archive, testdata_archive_path}; @@ -167,22 +167,26 @@ async fn restore_old_archive() { // Check that mtimes are restored. The sub-second times are not tested // because their behavior might vary depending on the local filesystem. - let file_mtime = metadata(dest.child("hello").path()) - .unwrap() - .modified() - .unwrap() - .to_timestamp(); + let file_mtime = Timestamp::try_from( + metadata(dest.child("hello").path()) + .unwrap() + .modified() + .unwrap(), + ) + .unwrap(); assert_eq!( file_mtime.as_second(), 1592266523, "mtime not restored correctly" ); - let dir_mtime = metadata(dest.child("subdir").path()) - .unwrap() - .modified() - .unwrap() - .to_timestamp(); + let dir_mtime = Timestamp::try_from( + metadata(dest.child("subdir").path()) + .unwrap() + .modified() + .unwrap(), + ) + .unwrap(); assert_eq!(dir_mtime.as_second(), 1592266523); } } From 6be03eb48c7fa6f9a9a00786594361367b006656 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:52:36 +0000 Subject: [PATCH 6/8] Remove timestamp_from_unix_nanos helper, use Timestamp::new directly Co-authored-by: sourcefrog <346355+sourcefrog@users.noreply.github.com> --- src/index/entry.rs | 3 +-- src/unix_time.rs | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/index/entry.rs b/src/index/entry.rs index 45eb0568..332f2f90 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -19,7 +19,6 @@ use crate::entry::EntryTrait; use crate::kind::Kind; use crate::owner::Owner; use crate::unix_mode::UnixMode; -use crate::unix_time::timestamp_from_unix_nanos; use crate::{blockdir, source}; /// Description of one archived file. @@ -84,7 +83,7 @@ impl EntryTrait for IndexEntry { #[inline] fn mtime(&self) -> Timestamp { - timestamp_from_unix_nanos(self.mtime, self.mtime_nanos) + Timestamp::new(self.mtime, self.mtime_nanos as i32).expect("valid timestamp") } /// Size of the file, if it is a file. None for directories and symlinks. diff --git a/src/unix_time.rs b/src/unix_time.rs index 58b34cae..a72eef30 100644 --- a/src/unix_time.rs +++ b/src/unix_time.rs @@ -18,14 +18,6 @@ use filetime::FileTime; use jiff::Timestamp; -/// Helper to construct a Timestamp from Unix seconds and nanoseconds. -pub(crate) fn timestamp_from_unix_nanos(unix_seconds: i64, nanoseconds: u32) -> Timestamp { - Timestamp::from_second(unix_seconds) - .unwrap() - .checked_add(jiff::Span::new().nanoseconds(nanoseconds as i64)) - .unwrap() -} - /// Helper to convert a Timestamp to a FileTime. pub(crate) fn timestamp_to_file_time(timestamp: &Timestamp) -> FileTime { FileTime::from_unix_time(timestamp.as_second(), timestamp.subsec_nanosecond() as u32) From 3962fc2efbdace61d01749642c48a73024be57bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:21:51 +0000 Subject: [PATCH 7/8] Run cargo fmt and add AGENTS.md with pre-commit guidelines Co-authored-by: sourcefrog <346355+sourcefrog@users.noreply.github.com> --- AGENTS.md | 17 +++++++++++++++++ src/band.rs | 20 ++++++++++---------- src/change.rs | 2 +- src/entry.rs | 2 +- src/index/entry.rs | 2 +- src/source/entry.rs | 2 +- src/transport/local.rs | 8 +++++++- src/transport/sftp.rs | 2 +- tests/s3_integration.rs | 2 +- 9 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..019796e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,17 @@ +# Agent Guidelines + +## Pre-commit Checklist + +Before committing any changes to this repository, always run: + +1. **`cargo fmt --check`** - Verify code is properly formatted + - If this fails, run `cargo fmt` to format the code +2. **`cargo check`** - Ensure code compiles without errors + +These commands help maintain code quality and consistency across the project. + +## Additional Quality Checks + +Consider also running: +- `cargo test` - Run all tests +- `cargo clippy` - Run linter for additional code quality checks diff --git a/src/band.rs b/src/band.rs index 57b32e77..dfd7b5f0 100644 --- a/src/band.rs +++ b/src/band.rs @@ -26,8 +26,8 @@ use std::sync::Arc; use crate::transport::Transport; use itertools::Itertools; -use serde::{Deserialize, Serialize}; use jiff::Timestamp; +use serde::{Deserialize, Serialize}; use tracing::{debug, trace, warn}; use crate::jsonio::{read_json, write_json}; @@ -267,18 +267,14 @@ impl Band { pub async fn get_info(&self) -> Result { let tail_option: Option = read_json(&self.transport, BAND_TAIL_FILENAME).await?; let start_time = - Timestamp::from_second(self.head.start_time).map_err(|_| { - Error::InvalidMetadata { - details: format!("Invalid band start timestamp {:?}", self.head.start_time), - } + Timestamp::from_second(self.head.start_time).map_err(|_| Error::InvalidMetadata { + details: format!("Invalid band start timestamp {:?}", self.head.start_time), })?; let end_time = tail_option .as_ref() .map(|tail| { - Timestamp::from_second(tail.end_time).map_err(|_| { - Error::InvalidMetadata { - details: format!("Invalid band end timestamp {:?}", tail.end_time), - } + Timestamp::from_second(tail.end_time).map_err(|_| Error::InvalidMetadata { + details: format!("Invalid band end timestamp {:?}", tail.end_time), }) }) .transpose()?; @@ -352,7 +348,11 @@ mod tests { assert_eq!(info.id.to_string(), "b0000"); assert!(info.is_closed); assert_eq!(info.index_hunk_count, Some(0)); - let dur = info.end_time.expect("info has an end_time").since(info.start_time).unwrap(); + let dur = info + .end_time + .expect("info has an end_time") + .since(info.start_time) + .unwrap(); // Test should have taken (much) less than 5s between starting and finishing // the band. (It might fail if you set a breakpoint right there.) assert!(dur.total(jiff::Unit::Second).unwrap() < 5.0); diff --git a/src/change.rs b/src/change.rs index c3de4ff7..02f8c251 100644 --- a/src/change.rs +++ b/src/change.rs @@ -15,8 +15,8 @@ use std::fmt; -use serde::Serialize; use jiff::Timestamp; +use serde::Serialize; use crate::{Apath, EntryTrait, Kind, Owner, Result, UnixMode}; diff --git a/src/entry.rs b/src/entry.rs index 2ba85785..db279a6f 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -16,8 +16,8 @@ use std::fmt::Debug; -use serde::Serialize; use jiff::Timestamp; +use serde::Serialize; use crate::*; diff --git a/src/index/entry.rs b/src/index/entry.rs index 332f2f90..4d58b981 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -11,8 +11,8 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -use serde_json::json; use jiff::Timestamp; +use serde_json::json; use crate::apath::Apath; use crate::entry::EntryTrait; diff --git a/src/source/entry.rs b/src/source/entry.rs index 2ac9d28e..77e2565c 100644 --- a/src/source/entry.rs +++ b/src/source/entry.rs @@ -1,5 +1,5 @@ -use serde::{self, Serialize}; use jiff::Timestamp; +use serde::{self, Serialize}; use crate::{Apath, EntryTrait, Kind, Owner, UnixMode, entry::KindMeta}; diff --git a/src/transport/local.rs b/src/transport/local.rs index 25f0ea16..35cbd2c7 100644 --- a/src/transport/local.rs +++ b/src/transport/local.rs @@ -307,7 +307,13 @@ mod test { assert_eq!(metadata.len, 24); assert_eq!(metadata.kind, Kind::File); - assert!(metadata.modified.checked_add(jiff::Span::new().seconds(60)).unwrap() > jiff::Timestamp::now()); + assert!( + metadata + .modified + .checked_add(jiff::Span::new().seconds(60)) + .unwrap() + > jiff::Timestamp::now() + ); assert!( transport .metadata("nopoem") diff --git a/src/transport/sftp.rs b/src/transport/sftp.rs index 5d78a559..d8cbb6fb 100644 --- a/src/transport/sftp.rs +++ b/src/transport/sftp.rs @@ -10,8 +10,8 @@ use std::sync::Arc; use async_trait::async_trait; use bytes::Bytes; -use ssh2::Sftp; use jiff::Timestamp; +use ssh2::Sftp; use tokio::task::spawn_blocking; use tracing::{error, info, trace, warn}; use url::Url; diff --git a/tests/s3_integration.rs b/tests/s3_integration.rs index 1c273496..d4f75225 100644 --- a/tests/s3_integration.rs +++ b/tests/s3_integration.rs @@ -28,8 +28,8 @@ use aws_sdk_s3::types::{ ExpirationStatus, LifecycleExpiration, LifecycleRule, LifecycleRuleFilter, }; use indoc::indoc; -use rand::Rng; use jiff::Timestamp; +use rand::Rng; use tokio::runtime::Runtime; struct TempBucket { From a012ea500c02b5c47d1cb988c804cfc6fc1c4bc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:37:28 +0000 Subject: [PATCH 8/8] Remove AGENTS.md from jiff PR AGENTS.md will be in a separate PR focused on contributor guidelines. This PR should focus only on the jiff migration and code formatting. --- AGENTS.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 019796e7..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,17 +0,0 @@ -# Agent Guidelines - -## Pre-commit Checklist - -Before committing any changes to this repository, always run: - -1. **`cargo fmt --check`** - Verify code is properly formatted - - If this fails, run `cargo fmt` to format the code -2. **`cargo check`** - Ensure code compiles without errors - -These commands help maintain code quality and consistency across the project. - -## Additional Quality Checks - -Consider also running: -- `cargo test` - Run all tests -- `cargo clippy` - Run linter for additional code quality checks