diff --git a/Cargo.lock b/Cargo.lock index b7f287a..38af009 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,15 +54,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.3.2" @@ -160,7 +151,6 @@ dependencies = [ "atuin-server", "atuin-server-postgres", "base64 0.21.2", - "chrono", "clap", "clap_complete", "colored", @@ -181,6 +171,7 @@ dependencies = [ "semver", "serde", "serde_json", + "time", "tiny-bip39", "tokio", "tracing", @@ -197,7 +188,6 @@ dependencies = [ "async-trait", "atuin-common", "base64 0.21.2", - "chrono", "clap", "config", "crypto_secretbox", @@ -229,6 +219,7 @@ dependencies = [ "shellexpand", "sql-builder", "sqlx", + "time", "tokio", "typed-builder", "urlencoding", @@ -240,12 +231,12 @@ dependencies = [ name = "atuin-common" version = "16.0.0" dependencies = [ - "chrono", "eyre", "pretty_assertions", "rand 0.8.5", "serde", "sqlx", + "time", "typed-builder", "uuid", ] @@ -260,8 +251,6 @@ dependencies = [ "atuin-server-database", "axum", "base64 0.21.2", - "chrono", - "chronoutil", "config", "eyre", "fs-err", @@ -271,6 +260,7 @@ dependencies = [ "semver", "serde", "serde_json", + "time", "tokio", "tower", "tower-http", @@ -284,10 +274,9 @@ version = "16.0.0" dependencies = [ "async-trait", "atuin-common", - "chrono", - "chronoutil", "eyre", "serde", + "time", "tracing", "uuid", ] @@ -299,10 +288,10 @@ dependencies = [ "async-trait", "atuin-common", "atuin-server-database", - "chrono", "futures-util", "serde", "sqlx", + "time", "tracing", "uuid", ] @@ -515,31 +504,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "chrono" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-integer", - "num-traits", - "serde", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "chronoutil" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a58c924bb772aa201da3acf5308c46b60275c64e6d3bc89c23dd63d71e83fd" -dependencies = [ - "chrono", -] - [[package]] name = "cipher" version = "0.3.0" @@ -838,6 +802,9 @@ name = "deranged" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +dependencies = [ + "serde", +] [[package]] name = "diff" @@ -1419,29 +1386,6 @@ dependencies = [ "tokio-rustls", ] -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "idna" version = "0.4.0" @@ -1521,8 +1465,8 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ffd2ac8397b9574daa4ffa7ede4427dd249cadaa900719d4b01154a5631d38b" dependencies = [ - "chrono", "logos", + "time", ] [[package]] @@ -1846,6 +1790,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -2478,7 +2431,7 @@ dependencies = [ "iso8601", "ring", "thiserror", - "time 0.3.25", + "time", "zeroize", ] @@ -2817,7 +2770,6 @@ dependencies = [ "atoi", "byteorder", "bytes", - "chrono", "crc", "crossbeam-queue", "dotenvy", @@ -2844,6 +2796,7 @@ dependencies = [ "smallvec", "sqlformat", "thiserror", + "time", "tokio", "tokio-stream", "tracing", @@ -2902,7 +2855,6 @@ dependencies = [ "bitflags 2.4.0", "byteorder", "bytes", - "chrono", "crc", "digest 0.10.7", "dotenvy", @@ -2930,6 +2882,7 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", + "time", "tracing", "uuid", "whoami", @@ -2945,7 +2898,6 @@ dependencies = [ "base64 0.21.2", "bitflags 2.4.0", "byteorder", - "chrono", "crc", "dotenvy", "etcetera", @@ -2971,6 +2923,7 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", + "time", "tracing", "uuid", "whoami", @@ -2983,7 +2936,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" dependencies = [ "atoi", - "chrono", "flume", "futures-channel", "futures-core", @@ -2995,6 +2947,7 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", + "time", "tracing", "url", "uuid", @@ -3104,23 +3057,14 @@ dependencies = [ [[package]] name = "time" -version = "0.1.45" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07" dependencies = [ "deranged", "itoa", + "libc", + "num_threads", "serde", "time-core", "time-macros", @@ -3134,9 +3078,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "75c65469ed6b3a4809d987a41eb1dc918e9bc1d92211cbad7ae82931846f7451" dependencies = [ "time-core", ] @@ -3523,12 +3467,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3661,15 +3599,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 9b9bfc0..1a8e1d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ name = "atuin" version = "16.0.0" authors = ["Ellie Huxtable "] -rust-version = "1.59" +rust-version = "1.67" license = "MIT" homepage = "https://atuin.sh" repository = "https://github.com/atuinsh/atuin" @@ -22,13 +22,13 @@ readme = "README.md" async-trait = "0.1.58" base64 = "0.21" log = "0.4" -chrono = { version = "0.4", features = ["serde"] } +time = { version = "0.3", features = ["serde-human-readable", "macros", "local-offset"] } clap = { version = "4.0.18", features = ["derive"] } config = { version = "0.13", default-features = false, features = ["toml"] } directories = "4" eyre = "0.6" fs-err = "2.9" -interim = { version = "0.1.0", features = ["chrono"] } +interim = { version = "0.1.0", features = ["time"] } itertools = "0.10.5" rand = { version = "0.8.5", features = ["std"] } semver = "1.0.14" @@ -50,4 +50,9 @@ default-features = false [workspace.dependencies.sqlx] version = "0.7.1" -features = ["runtime-tokio-rustls", "chrono", "postgres", "uuid"] +features = [ + "runtime-tokio-rustls", + "time", + "postgres", + "uuid", +] diff --git a/atuin-client/Cargo.toml b/atuin-client/Cargo.toml index 732b6a7..9e8a050 100644 --- a/atuin-client/Cargo.toml +++ b/atuin-client/Cargo.toml @@ -3,6 +3,7 @@ name = "atuin-client" edition = "2021" description = "client library for atuin" +rust-version = { workspace = true } version = { workspace = true } authors = { workspace = true } license = { workspace = true } @@ -20,7 +21,7 @@ atuin-common = { path = "../atuin-common", version = "16.0.0" } log = { workspace = true } base64 = { workspace = true } -chrono = { workspace = true } +time = { workspace = true } clap = { workspace = true } eyre = { workspace = true } directories = { workspace = true } diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs index 5ae1ed0..d2ca339 100644 --- a/atuin-client/src/api_client.rs +++ b/atuin-client/src/api_client.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::env; -use chrono::Utc; use eyre::{bail, Result}; use reqwest::{ header::{HeaderMap, AUTHORIZATION, USER_AGENT}, @@ -17,6 +16,8 @@ use atuin_common::{ record::RecordIndex, }; use semver::Version; +use time::format_description::well_known::Rfc3339; +use time::OffsetDateTime; use crate::{history::History, sync::hash_str}; @@ -150,8 +151,8 @@ impl<'a> Client<'a> { pub async fn get_history( &self, - sync_ts: chrono::DateTime, - history_ts: chrono::DateTime, + sync_ts: OffsetDateTime, + history_ts: OffsetDateTime, host: Option, ) -> Result { let host = host.unwrap_or_else(|| { @@ -165,8 +166,8 @@ impl<'a> Client<'a> { let url = format!( "{}/sync/history?sync_ts={}&history_ts={}&host={}", self.sync_addr, - urlencoding::encode(sync_ts.to_rfc3339().as_str()), - urlencoding::encode(history_ts.to_rfc3339().as_str()), + urlencoding::encode(sync_ts.format(&Rfc3339)?.as_str()), + urlencoding::encode(history_ts.format(&Rfc3339)?.as_str()), host, ); diff --git a/atuin-client/src/database.rs b/atuin-client/src/database.rs index b69c7cb..16a4a43 100644 --- a/atuin-client/src/database.rs +++ b/atuin-client/src/database.rs @@ -6,7 +6,6 @@ use std::{ use async_trait::async_trait; use atuin_common::utils; -use chrono::{prelude::*, Utc}; use fs_err as fs; use itertools::Itertools; use lazy_static::lazy_static; @@ -17,6 +16,7 @@ use sqlx::{ sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow}, Result, Row, }; +use time::OffsetDateTime; use super::{ history::History, @@ -81,18 +81,14 @@ pub trait Database: Send + Sync + 'static { max: Option, unique: bool, ) -> Result>; - async fn range( - &self, - from: chrono::DateTime, - to: chrono::DateTime, - ) -> Result>; + async fn range(&self, from: OffsetDateTime, to: OffsetDateTime) -> Result>; async fn update(&self, h: &History) -> Result<()>; async fn history_count(&self) -> Result; async fn first(&self) -> Result; async fn last(&self) -> Result; - async fn before(&self, timestamp: chrono::DateTime, count: i64) -> Result>; + async fn before(&self, timestamp: OffsetDateTime, count: i64) -> Result>; async fn delete(&self, mut h: History) -> Result<()>; async fn deleted(&self) -> Result>; @@ -158,14 +154,14 @@ impl Sqlite { values(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", ) .bind(h.id.as_str()) - .bind(h.timestamp.timestamp_nanos()) + .bind(h.timestamp.unix_timestamp_nanos() as i64) .bind(h.duration) .bind(h.exit) .bind(h.command.as_str()) .bind(h.cwd.as_str()) .bind(h.session.as_str()) .bind(h.hostname.as_str()) - .bind(h.deleted_at.map(|t|t.timestamp_nanos())) + .bind(h.deleted_at.map(|t|t.unix_timestamp_nanos() as i64)) .execute(&mut **tx) .await?; @@ -177,14 +173,19 @@ impl Sqlite { History::from_db() .id(row.get("id")) - .timestamp(Utc.timestamp_nanos(row.get("timestamp"))) + .timestamp( + OffsetDateTime::from_unix_timestamp_nanos(row.get::("timestamp") as i128) + .unwrap(), + ) .duration(row.get("duration")) .exit(row.get("exit")) .command(row.get("command")) .cwd(row.get("cwd")) .session(row.get("session")) .hostname(row.get("hostname")) - .deleted_at(deleted_at.map(|t| Utc.timestamp_nanos(t))) + .deleted_at( + deleted_at.and_then(|t| OffsetDateTime::from_unix_timestamp_nanos(t as i128).ok()), + ) .build() .into() } @@ -236,14 +237,14 @@ impl Database for Sqlite { where id = ?1", ) .bind(h.id.as_str()) - .bind(h.timestamp.timestamp_nanos()) + .bind(h.timestamp.unix_timestamp_nanos() as i64) .bind(h.duration) .bind(h.exit) .bind(h.command.as_str()) .bind(h.cwd.as_str()) .bind(h.session.as_str()) .bind(h.hostname.as_str()) - .bind(h.deleted_at.map(|t|t.timestamp_nanos())) + .bind(h.deleted_at.map(|t|t.unix_timestamp_nanos() as i64)) .execute(&self.pool) .await?; @@ -292,18 +293,14 @@ impl Database for Sqlite { Ok(res) } - async fn range( - &self, - from: chrono::DateTime, - to: chrono::DateTime, - ) -> Result> { + async fn range(&self, from: OffsetDateTime, to: OffsetDateTime) -> Result> { debug!("listing history from {:?} to {:?}", from, to); let res = sqlx::query( "select * from history where timestamp >= ?1 and timestamp <= ?2 order by timestamp asc", ) - .bind(from.timestamp_nanos()) - .bind(to.timestamp_nanos()) + .bind(from.unix_timestamp_nanos() as i64) + .bind(to.unix_timestamp_nanos() as i64) .map(Self::query_history) .fetch_all(&self.pool) .await?; @@ -332,11 +329,11 @@ impl Database for Sqlite { Ok(res) } - async fn before(&self, timestamp: chrono::DateTime, count: i64) -> Result> { + async fn before(&self, timestamp: OffsetDateTime, count: i64) -> Result> { let res = sqlx::query( "select * from history where timestamp < ?1 order by timestamp desc limit ?2", ) - .bind(timestamp.timestamp_nanos()) + .bind(timestamp.unix_timestamp_nanos() as i64) .bind(count) .map(Self::query_history) .fetch_all(&self.pool) @@ -471,13 +468,23 @@ impl Database for Sqlite { .map(|exclude_cwd| sql.and_where_ne("cwd", quote(exclude_cwd))); filter_options.before.map(|before| { - interim::parse_date_string(before.as_str(), Utc::now(), interim::Dialect::Uk) - .map(|before| sql.and_where_lt("timestamp", quote(before.timestamp_nanos()))) + interim::parse_date_string( + before.as_str(), + OffsetDateTime::now_utc(), + interim::Dialect::Uk, + ) + .map(|before| { + sql.and_where_lt("timestamp", quote(before.unix_timestamp_nanos() as i64)) + }) }); filter_options.after.map(|after| { - interim::parse_date_string(after.as_str(), Utc::now(), interim::Dialect::Uk) - .map(|after| sql.and_where_gt("timestamp", quote(after.timestamp_nanos()))) + interim::parse_date_string( + after.as_str(), + OffsetDateTime::now_utc(), + interim::Dialect::Uk, + ) + .map(|after| sql.and_where_gt("timestamp", quote(after.unix_timestamp_nanos() as i64))) }); sql.and_where_is_null("deleted_at"); @@ -540,7 +547,7 @@ impl Database for Sqlite { // deleted_at doesn't mean the actual time that the user deleted it, // but the time that the system marks it as deleted async fn delete(&self, mut h: History) -> Result<()> { - let now = chrono::Utc::now(); + let now = OffsetDateTime::now_utc(); h.command = rand::thread_rng() .sample_iter(&Alphanumeric) .take(32) @@ -612,7 +619,7 @@ mod test { async fn new_history_item(db: &mut impl Database, cmd: &str) -> Result<()> { let mut captured: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(OffsetDateTime::now_utc()) .command(cmd) .cwd("/home/ellie") .build() diff --git a/atuin-client/src/encryption.rs b/atuin-client/src/encryption.rs index 056c56d..f9f8bcb 100644 --- a/atuin-client/src/encryption.rs +++ b/atuin-client/src/encryption.rs @@ -11,7 +11,6 @@ use std::{io::prelude::*, path::PathBuf}; use base64::prelude::{Engine, BASE64_STANDARD}; -use chrono::{DateTime, Utc}; pub use crypto_secretbox::Key; use crypto_secretbox::{ aead::{Nonce, OsRng}, @@ -21,6 +20,7 @@ use eyre::{bail, ensure, eyre, Context, Result}; use fs_err as fs; use rmp::{decode::Bytes, Marker}; use serde::{Deserialize, Serialize}; +use time::{format_description::well_known::Rfc3339, macros::format_description, OffsetDateTime}; use crate::{history::History, settings::Settings}; @@ -137,6 +137,28 @@ pub fn decrypt(mut encrypted_history: EncryptedHistory, key: &Key) -> Result Result { + // horrible hack. chrono AutoSI limits to 0, 3, 6, or 9 decimal places for nanoseconds. + // time does not have this functionality. + static PARTIAL_RFC3339_0: &[time::format_description::FormatItem<'static>] = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"); + static PARTIAL_RFC3339_3: &[time::format_description::FormatItem<'static>] = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:3]Z"); + static PARTIAL_RFC3339_6: &[time::format_description::FormatItem<'static>] = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6]Z"); + static PARTIAL_RFC3339_9: &[time::format_description::FormatItem<'static>] = + format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:9]Z"); + + let fmt = match ts.nanosecond() { + 0 => PARTIAL_RFC3339_0, + ns if ns % 1_000_000 == 0 => PARTIAL_RFC3339_3, + ns if ns % 1_000 == 0 => PARTIAL_RFC3339_6, + _ => PARTIAL_RFC3339_9, + }; + + Ok(ts.format(fmt)?) +} + fn encode(h: &History) -> Result> { use rmp::encode; @@ -145,11 +167,7 @@ fn encode(h: &History) -> Result> { encode::write_array_len(&mut output, 9)?; encode::write_str(&mut output, &h.id)?; - encode::write_str( - &mut output, - &(h.timestamp - .to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true)), - )?; + encode::write_str(&mut output, &(format_rfc3339(h.timestamp)?))?; encode::write_sint(&mut output, h.duration)?; encode::write_sint(&mut output, h.exit)?; encode::write_str(&mut output, &h.command)?; @@ -157,10 +175,7 @@ fn encode(h: &History) -> Result> { encode::write_str(&mut output, &h.session)?; encode::write_str(&mut output, &h.hostname)?; match h.deleted_at { - Some(d) => encode::write_str( - &mut output, - &d.to_rfc3339_opts(chrono::SecondsFormat::AutoSi, true), - )?, + Some(d) => encode::write_str(&mut output, &format_rfc3339(d)?)?, None => encode::write_nil(&mut output)?, } @@ -220,7 +235,7 @@ fn decode(bytes: &[u8]) -> Result { Ok(History { id: id.to_owned(), - timestamp: DateTime::parse_from_rfc3339(timestamp)?.with_timezone(&Utc), + timestamp: OffsetDateTime::parse(timestamp, &Rfc3339)?, duration, exit, command: command.to_owned(), @@ -228,9 +243,8 @@ fn decode(bytes: &[u8]) -> Result { session: session.to_owned(), hostname: hostname.to_owned(), deleted_at: deleted_at - .map(DateTime::parse_from_rfc3339) - .transpose()? - .map(|dt| dt.with_timezone(&Utc)), + .map(|t| OffsetDateTime::parse(t, &Rfc3339)) + .transpose()?, }) } @@ -241,6 +255,8 @@ fn error_report(err: E) -> eyre::Report { #[cfg(test)] mod test { use crypto_secretbox::{aead::OsRng, KeyInit, XSalsa20Poly1305}; + use pretty_assertions::assert_eq; + use time::{macros::datetime, OffsetDateTime}; use crate::history::History; @@ -253,7 +269,7 @@ mod test { let history = History::from_db() .id("1".into()) - .timestamp(chrono::Utc::now()) + .timestamp(OffsetDateTime::now_utc()) .command("ls".into()) .cwd("/home/ellie".into()) .exit(0) @@ -297,7 +313,7 @@ mod test { ]; let history = History { id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned(), - timestamp: "2023-05-28T18:35:40.633872Z".parse().unwrap(), + timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), duration: 49206000, exit: 0, command: "git status".to_owned(), @@ -318,14 +334,14 @@ mod test { fn test_decode_deleted() { let history = History { id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned(), - timestamp: "2023-05-28T18:35:40.633872Z".parse().unwrap(), + timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), duration: 49206000, exit: 0, command: "git status".to_owned(), cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), session: "b97d9a306f274473a203d2eba41f9457".to_owned(), hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), - deleted_at: Some("2023-05-28T18:35:40.633872Z".parse().unwrap()), + deleted_at: Some(datetime!(2023-05-28 18:35:40.633872 +00:00)), }; let b = encode(&history).unwrap(); @@ -349,7 +365,7 @@ mod test { ]; let history = History { id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned(), - timestamp: "2023-05-28T18:35:40.633872Z".parse().unwrap(), + timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), duration: 49206000, exit: 0, command: "git status".to_owned(), diff --git a/atuin-client/src/history.rs b/atuin-client/src/history.rs index 4d08478..f4c0a8e 100644 --- a/atuin-client/src/history.rs +++ b/atuin-client/src/history.rs @@ -1,11 +1,10 @@ use std::env; -use chrono::Utc; - use atuin_common::utils::uuid_v7; use regex::RegexSet; use crate::{secrets::SECRET_PATTERNS, settings::Settings}; +use time::OffsetDateTime; mod builder; @@ -29,7 +28,7 @@ pub struct History { /// Stored as `client_id` in the database. pub id: String, /// When the command was run. - pub timestamp: chrono::DateTime, + pub timestamp: OffsetDateTime, /// How long the command took to run. pub duration: i64, /// The exit code of the command. @@ -43,20 +42,20 @@ pub struct History { /// The hostname of the machine the command was run on. pub hostname: String, /// Timestamp, which is set when the entry is deleted, allowing a soft delete. - pub deleted_at: Option>, + pub deleted_at: Option, } impl History { #[allow(clippy::too_many_arguments)] fn new( - timestamp: chrono::DateTime, + timestamp: OffsetDateTime, command: String, cwd: String, exit: i64, duration: i64, session: Option, hostname: Option, - deleted_at: Option>, + deleted_at: Option, ) -> Self { let session = session .or_else(|| env::var("ATUIN_SESSION").ok()) @@ -91,7 +90,7 @@ impl History { /// use atuin_client::history::History; /// /// let history: History = History::import() - /// .timestamp(chrono::Utc::now()) + /// .timestamp(time::OffsetDateTime::now_utc()) /// .command("ls -la") /// .build() /// .into(); @@ -102,7 +101,7 @@ impl History { /// use atuin_client::history::History; /// /// let history: History = History::import() - /// .timestamp(chrono::Utc::now()) + /// .timestamp(time::OffsetDateTime::now_utc()) /// .command("ls -la") /// .cwd("/home/user") /// .exit(0) @@ -138,7 +137,7 @@ impl History { /// use atuin_client::history::History; /// /// let history: History = History::capture() - /// .timestamp(chrono::Utc::now()) + /// .timestamp(time::OffsetDateTime::now_utc()) /// .command("ls -la") /// .cwd("/home/user") /// .build() @@ -152,7 +151,7 @@ impl History { /// /// // this will not compile because `cwd` is missing /// let history: History = History::capture() - /// .timestamp(chrono::Utc::now()) + /// .timestamp(time::OffsetDateTime::now_utc()) /// .command("ls -la") /// .build() /// .into(); @@ -170,7 +169,7 @@ impl History { /// /// // this will not compile because `id` field is missing /// let history: History = History::from_db() - /// .timestamp(chrono::Utc::now()) + /// .timestamp(time::OffsetDateTime::now_utc()) /// .command("ls -la".to_string()) /// .cwd("/home/user".to_string()) /// .exit(0) @@ -216,35 +215,35 @@ mod tests { settings.history_filter = RegexSet::new(["^psql"]).unwrap(); let normal_command: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(time::OffsetDateTime::now_utc()) .command("echo foo") .cwd("/") .build() .into(); let with_space: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(time::OffsetDateTime::now_utc()) .command(" echo bar") .cwd("/") .build() .into(); let stripe_key: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(time::OffsetDateTime::now_utc()) .command("curl foo.com/bar?key=sk_test_1234567890abcdefghijklmnop") .cwd("/") .build() .into(); let secret_dir: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(time::OffsetDateTime::now_utc()) .command("echo ohno") .cwd("/supasecret") .build() .into(); let with_psql: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(time::OffsetDateTime::now_utc()) .command("psql") .cwd("/supasecret") .build() @@ -263,7 +262,7 @@ mod tests { settings.secrets_filter = false; let stripe_key: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(time::OffsetDateTime::now_utc()) .command("curl foo.com/bar?key=sk_test_1234567890abcdefghijklmnop") .cwd("/") .build() diff --git a/atuin-client/src/history/builder.rs b/atuin-client/src/history/builder.rs index dc22b60..27531c9 100644 --- a/atuin-client/src/history/builder.rs +++ b/atuin-client/src/history/builder.rs @@ -1,4 +1,3 @@ -use chrono::Utc; use typed_builder::TypedBuilder; use super::History; @@ -8,7 +7,7 @@ use super::History; /// The only two required fields are `timestamp` and `command`. #[derive(Debug, Clone, TypedBuilder)] pub struct HistoryImported { - timestamp: chrono::DateTime, + timestamp: time::OffsetDateTime, #[builder(setter(into))] command: String, #[builder(default = "unknown".into(), setter(into))] @@ -45,7 +44,7 @@ impl From for History { /// the command is finished, such as `exit` or `duration`. #[derive(Debug, Clone, TypedBuilder)] pub struct HistoryCaptured { - timestamp: chrono::DateTime, + timestamp: time::OffsetDateTime, #[builder(setter(into))] command: String, #[builder(setter(into))] @@ -73,14 +72,14 @@ impl From for History { #[derive(Debug, Clone, TypedBuilder)] pub struct HistoryFromDb { id: String, - timestamp: chrono::DateTime, + timestamp: time::OffsetDateTime, command: String, cwd: String, exit: i64, duration: i64, session: String, hostname: String, - deleted_at: Option>, + deleted_at: Option, } impl From for History { diff --git a/atuin-client/src/import/bash.rs b/atuin-client/src/import/bash.rs index 25ede05..fe080a5 100644 --- a/atuin-client/src/import/bash.rs +++ b/atuin-client/src/import/bash.rs @@ -1,10 +1,10 @@ use std::{fs::File, io::Read, path::PathBuf, str}; use async_trait::async_trait; -use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use directories::UserDirs; use eyre::{eyre, Result}; use itertools::Itertools; +use time::{Duration, OffsetDateTime}; use super::{get_histpath, unix_byte_lines, Importer, Loader}; use crate::history::History; @@ -55,7 +55,7 @@ impl Importer for Bash { _ => None, }) // if no known timestamps, use now as base - .unwrap_or((lines.len(), Utc::now())); + .unwrap_or((lines.len(), OffsetDateTime::now_utc())); // if no timestamp is recorded, then use this increment to set an arbitrary timestamp // to preserve ordering @@ -99,7 +99,7 @@ enum LineType<'a> { Empty, /// A timestamp line start with a '#', followed immediately by an integer /// that represents seconds since UNIX epoch. - Timestamp(DateTime), + Timestamp(OffsetDateTime), /// Anything else. Command(&'a str), } @@ -119,10 +119,9 @@ impl<'a> From<&'a [u8]> for LineType<'a> { } } -fn try_parse_line_as_timestamp(line: &str) -> Option> { +fn try_parse_line_as_timestamp(line: &str) -> Option { let seconds = line.strip_prefix('#')?.parse().ok()?; - let time = NaiveDateTime::from_timestamp(seconds, 0); - Some(DateTime::from_utc(time, Utc)) + OffsetDateTime::from_unix_timestamp(seconds).ok() } #[cfg(test)] @@ -183,7 +182,7 @@ cd ../ ["git reset", "git clean -dxf", "cd ../"], ); assert_equal( - loader.buf.iter().map(|h| h.timestamp.timestamp()), + loader.buf.iter().map(|h| h.timestamp.unix_timestamp()), [1672918999, 1672919006, 1672919020], ) } diff --git a/atuin-client/src/import/fish.rs b/atuin-client/src/import/fish.rs index 90ecabc..82c5901 100644 --- a/atuin-client/src/import/fish.rs +++ b/atuin-client/src/import/fish.rs @@ -4,9 +4,9 @@ use std::{fs::File, io::Read, path::PathBuf}; use async_trait::async_trait; -use chrono::{prelude::*, Utc}; use directories::BaseDirs; use eyre::{eyre, Result}; +use time::OffsetDateTime; use super::{unix_byte_lines, Importer, Loader}; use crate::history::History; @@ -59,8 +59,8 @@ impl Importer for Fish { } async fn load(self, loader: &mut impl Loader) -> Result<()> { - let now = Utc::now(); - let mut time: Option> = None; + let now = OffsetDateTime::now_utc(); + let mut time: Option = None; let mut cmd: Option = None; for b in unix_byte_lines(&self.bytes) { @@ -89,7 +89,7 @@ impl Importer for Fish { } else if let Some(t) = s.strip_prefix(" when: ") { // if t is not an int, just ignore this line if let Ok(t) = t.parse::() { - time = Some(Utc.timestamp(t, 0)); + time = Some(OffsetDateTime::from_unix_timestamp(t)?); } } else { // ... ignore paths lines @@ -164,7 +164,7 @@ ERROR ($timestamp:expr, $command:expr) => { let h = history.next().expect("missing entry in history"); assert_eq!(h.command.as_str(), $command); - assert_eq!(h.timestamp.timestamp(), $timestamp); + assert_eq!(h.timestamp.unix_timestamp(), $timestamp); }; } diff --git a/atuin-client/src/import/nu.rs b/atuin-client/src/import/nu.rs index 4660032..1131fac 100644 --- a/atuin-client/src/import/nu.rs +++ b/atuin-client/src/import/nu.rs @@ -6,6 +6,7 @@ use std::{fs::File, io::Read, path::PathBuf}; use async_trait::async_trait; use directories::BaseDirs; use eyre::{eyre, Result}; +use time::OffsetDateTime; use super::{unix_byte_lines, Importer, Loader}; use crate::history::History; @@ -44,7 +45,7 @@ impl Importer for Nu { } async fn load(self, h: &mut impl Loader) -> Result<()> { - let now = chrono::Utc::now(); + let now = OffsetDateTime::now_utc(); let mut counter = 0; for b in unix_byte_lines(&self.bytes) { @@ -55,7 +56,7 @@ impl Importer for Nu { let cmd: String = s.replace("<\\n>", "\n"); - let offset = chrono::Duration::nanoseconds(counter); + let offset = time::Duration::nanoseconds(counter); counter += 1; let entry = History::import().timestamp(now - offset).command(cmd); diff --git a/atuin-client/src/import/nu_histdb.rs b/atuin-client/src/import/nu_histdb.rs index 34568d8..f0e8e95 100644 --- a/atuin-client/src/import/nu_histdb.rs +++ b/atuin-client/src/import/nu_histdb.rs @@ -4,10 +4,10 @@ use std::path::PathBuf; use async_trait::async_trait; -use chrono::{prelude::*, Utc}; use directories::BaseDirs; use eyre::{eyre, Result}; use sqlx::{sqlite::SqlitePool, Pool}; +use time::{Duration, OffsetDateTime}; use super::Importer; use crate::history::History; @@ -31,10 +31,10 @@ impl From for History { let ts_secs = histdb_item.start_timestamp / 1000; let ts_ns = (histdb_item.start_timestamp % 1000) * 1_000_000; let imported = History::import() - .timestamp(DateTime::from_utc( - NaiveDateTime::from_timestamp(ts_secs, ts_ns as u32), - Utc, - )) + .timestamp( + OffsetDateTime::from_unix_timestamp(ts_secs).unwrap() + + Duration::nanoseconds(ts_ns), + ) .command(String::from_utf8(histdb_item.command_line).unwrap()) .cwd(String::from_utf8(histdb_item.cwd).unwrap()) .exit(histdb_item.exit_status) diff --git a/atuin-client/src/import/resh.rs b/atuin-client/src/import/resh.rs index 3c5b799..5475db5 100644 --- a/atuin-client/src/import/resh.rs +++ b/atuin-client/src/import/resh.rs @@ -1,12 +1,12 @@ use std::{fs::File, io::Read, path::PathBuf}; use async_trait::async_trait; -use chrono::{TimeZone, Utc}; use directories::UserDirs; use eyre::{eyre, Result}; use serde::Deserialize; use atuin_common::utils::uuid_v7; +use time::OffsetDateTime; use super::{get_histpath, unix_byte_lines, Importer, Loader}; use crate::history::History; @@ -110,16 +110,18 @@ impl Importer for Resh { #[allow(clippy::cast_sign_loss)] let timestamp = { let secs = entry.realtime_before.floor() as i64; - let nanosecs = (entry.realtime_before.fract() * 1_000_000_000_f64).round() as u32; - Utc.timestamp(secs, nanosecs) + let nanosecs = (entry.realtime_before.fract() * 1_000_000_000_f64).round() as i64; + OffsetDateTime::from_unix_timestamp(secs)? + time::Duration::nanoseconds(nanosecs) }; #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] let duration = { let secs = entry.realtime_after.floor() as i64; - let nanosecs = (entry.realtime_after.fract() * 1_000_000_000_f64).round() as u32; - let difference = Utc.timestamp(secs, nanosecs) - timestamp; - difference.num_nanoseconds().unwrap_or(0) + let nanosecs = (entry.realtime_after.fract() * 1_000_000_000_f64).round() as i64; + let base = OffsetDateTime::from_unix_timestamp(secs)? + + time::Duration::nanoseconds(nanosecs); + let difference = base - timestamp; + difference.whole_nanoseconds() as i64 }; let imported = History::import() diff --git a/atuin-client/src/import/zsh.rs b/atuin-client/src/import/zsh.rs index e98819e..632caff 100644 --- a/atuin-client/src/import/zsh.rs +++ b/atuin-client/src/import/zsh.rs @@ -4,9 +4,9 @@ use std::{fs::File, io::Read, path::PathBuf}; use async_trait::async_trait; -use chrono::{prelude::*, Utc}; use directories::UserDirs; use eyre::{eyre, Result}; +use time::OffsetDateTime; use super::{get_histpath, unix_byte_lines, Importer, Loader}; use crate::history::History; @@ -58,7 +58,7 @@ impl Importer for Zsh { } async fn load(self, h: &mut impl Loader) -> Result<()> { - let now = chrono::Utc::now(); + let now = OffsetDateTime::now_utc(); let mut line = String::new(); let mut counter = 0; @@ -79,7 +79,7 @@ impl Importer for Zsh { counter += 1; h.push(parse_extended(command, counter)).await?; } else { - let offset = chrono::Duration::seconds(counter); + let offset = time::Duration::seconds(counter); counter += 1; let imported = History::import() @@ -102,11 +102,10 @@ fn parse_extended(line: &str, counter: i64) -> History { let time = time .parse::() - .unwrap_or_else(|_| chrono::Utc::now().timestamp()); - - let offset = chrono::Duration::milliseconds(counter); - let time = Utc.timestamp(time, 0); - let time = time + offset; + .ok() + .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()) + .unwrap_or_else(OffsetDateTime::now_utc) + + time::Duration::milliseconds(counter); // use nanos, because why the hell not? we won't display them. let duration = duration.parse::().map_or(-1, |t| t * 1_000_000_000); @@ -121,8 +120,6 @@ fn parse_extended(line: &str, counter: i64) -> History { #[cfg(test)] mod test { - use chrono::prelude::*; - use chrono::Utc; use itertools::assert_equal; use crate::import::tests::TestLoader; @@ -135,25 +132,37 @@ mod test { assert_eq!(parsed.command, "cargo install atuin"); assert_eq!(parsed.duration, 0); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + assert_eq!( + parsed.timestamp, + OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap() + ); let parsed = parse_extended("1613322469:10;cargo install atuin;cargo update", 0); assert_eq!(parsed.command, "cargo install atuin;cargo update"); assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + assert_eq!( + parsed.timestamp, + OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap() + ); let parsed = parse_extended("1613322469:10;cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", 0); assert_eq!(parsed.command, "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷"); assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + assert_eq!( + parsed.timestamp, + OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap() + ); let parsed = parse_extended("1613322469:10;cargo install \\n atuin\n", 0); assert_eq!(parsed.command, "cargo install \\n atuin"); assert_eq!(parsed.duration, 10_000_000_000); - assert_eq!(parsed.timestamp, Utc.timestamp(1_613_322_469, 0)); + assert_eq!( + parsed.timestamp, + OffsetDateTime::from_unix_timestamp(1_613_322_469).unwrap() + ); } #[tokio::test] diff --git a/atuin-client/src/import/zsh_histdb.rs b/atuin-client/src/import/zsh_histdb.rs index 78a7176..37c7081 100644 --- a/atuin-client/src/import/zsh_histdb.rs +++ b/atuin-client/src/import/zsh_histdb.rs @@ -35,10 +35,10 @@ use std::path::{Path, PathBuf}; use async_trait::async_trait; -use chrono::{prelude::*, Utc}; use directories::UserDirs; use eyre::{eyre, Result}; use sqlx::{sqlite::SqlitePool, Pool}; +use time::PrimitiveDateTime; use super::Importer; use crate::history::History; @@ -52,7 +52,7 @@ pub struct HistDbEntryCount { #[derive(sqlx::FromRow, Debug)] pub struct HistDbEntry { pub id: i64, - pub start_time: NaiveDateTime, + pub start_time: PrimitiveDateTime, pub host: Vec, pub dir: Vec, pub argv: Vec, @@ -62,7 +62,7 @@ pub struct HistDbEntry { impl From for History { fn from(histdb_item: HistDbEntry) -> Self { let imported = History::import() - .timestamp(DateTime::from_utc(histdb_item.start_time, Utc)) + .timestamp(histdb_item.start_time.assume_utc()) .command( String::from_utf8(histdb_item.argv) .unwrap_or_else(|_e| String::from("")) diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs index c68be0d..ebc1a37 100644 --- a/atuin-client/src/settings.rs +++ b/atuin-client/src/settings.rs @@ -1,11 +1,11 @@ use std::{ + convert::TryFrom, io::prelude::*, path::{Path, PathBuf}, str::FromStr, }; use atuin_common::record::HostId; -use chrono::{prelude::*, Utc}; use clap::ValueEnum; use config::{ builder::DefaultState, Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat, @@ -16,6 +16,7 @@ use parse_duration::parse; use regex::RegexSet; use semver::Version; use serde::Deserialize; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use uuid::Uuid; pub const HISTORY_PAGE_SIZE: i64 = 100; @@ -207,21 +208,20 @@ impl Settings { } fn save_current_time(filename: &str) -> Result<()> { - Settings::save_to_data_dir(filename, Utc::now().to_rfc3339().as_str())?; + Settings::save_to_data_dir( + filename, + OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(), + )?; Ok(()) } - fn load_time_from_file(filename: &str) -> Result> { + fn load_time_from_file(filename: &str) -> Result { let value = Settings::read_from_data_dir(filename); match value { - Some(v) => { - let time = chrono::DateTime::parse_from_rfc3339(v.as_str())?; - - Ok(time.with_timezone(&Utc)) - } - None => Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), + Some(v) => Ok(OffsetDateTime::parse(v.as_str(), &Rfc3339)?), + None => Ok(OffsetDateTime::UNIX_EPOCH), } } @@ -233,11 +233,11 @@ impl Settings { Settings::save_current_time(LAST_VERSION_CHECK_FILENAME) } - pub fn last_sync() -> Result> { + pub fn last_sync() -> Result { Settings::load_time_from_file(LAST_SYNC_FILENAME) } - pub fn last_version_check() -> Result> { + pub fn last_version_check() -> Result { Settings::load_time_from_file(LAST_VERSION_CHECK_FILENAME) } @@ -265,8 +265,8 @@ impl Settings { match parse(self.sync_frequency.as_str()) { Ok(d) => { - let d = chrono::Duration::from_std(d).unwrap(); - Ok(Utc::now() - Settings::last_sync()? >= d) + let d = time::Duration::try_from(d).unwrap(); + Ok(OffsetDateTime::now_utc() - Settings::last_sync()? >= d) } Err(e) => Err(eyre!("failed to check sync: {}", e)), } @@ -274,10 +274,10 @@ impl Settings { fn needs_update_check(&self) -> Result { let last_check = Settings::last_version_check()?; - let diff = Utc::now() - last_check; + let diff = OffsetDateTime::now_utc() - last_check; // Check a max of once per hour - Ok(diff.num_hours() >= 1) + Ok(diff.whole_hours() >= 1) } async fn latest_version(&self) -> Result { diff --git a/atuin-client/src/sync.rs b/atuin-client/src/sync.rs index 6704ad5..439ed6d 100644 --- a/atuin-client/src/sync.rs +++ b/atuin-client/src/sync.rs @@ -2,11 +2,11 @@ use std::collections::HashSet; use std::convert::TryInto; use std::iter::FromIterator; -use chrono::prelude::*; use eyre::Result; use atuin_common::api::AddHistoryRequest; use crypto_secretbox::Key; +use time::OffsetDateTime; use crate::{ api_client, @@ -52,12 +52,12 @@ async fn sync_download( let mut local_count = initial_local; let mut last_sync = if force { - Utc.timestamp_millis(0) + OffsetDateTime::UNIX_EPOCH } else { Settings::last_sync()? }; - let mut last_timestamp = Utc.timestamp_millis(0); + let mut last_timestamp = OffsetDateTime::UNIX_EPOCH; let host = if force { Some(String::from("")) } else { None }; @@ -74,7 +74,7 @@ async fn sync_download( .map(|h| decrypt(h, key).expect("failed to decrypt history! check your key")) .map(|mut h| { if remote_deleted.contains(h.id.as_str()) { - h.deleted_at = Some(chrono::Utc::now()); + h.deleted_at = Some(time::OffsetDateTime::now_utc()); h.command = String::from(""); } @@ -99,8 +99,8 @@ async fn sync_download( // be "lost" between syncs. In this case we need to rewind the sync // timestamps if page_last == last_timestamp { - last_timestamp = Utc.timestamp_millis(0); - last_sync -= chrono::Duration::hours(1); + last_timestamp = OffsetDateTime::UNIX_EPOCH; + last_sync -= time::Duration::hours(1); } else { last_timestamp = page_last; } @@ -142,7 +142,7 @@ async fn sync_upload( debug!("remote has {}, we have {}", remote_count, local_count); // first just try the most recent set - let mut cursor = Utc::now(); + let mut cursor = OffsetDateTime::now_utc(); while local_count > remote_count { let last = db.before(cursor, remote_status.page_size).await?; diff --git a/atuin-common/Cargo.toml b/atuin-common/Cargo.toml index a610584..88c3022 100644 --- a/atuin-common/Cargo.toml +++ b/atuin-common/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "atuin-common" -edition = "2018" +edition = "2021" description = "common library for atuin" +rust-version = { workspace = true } version = { workspace = true } authors = { workspace = true } license = { workspace = true } @@ -12,7 +13,7 @@ repository = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { workspace = true } +time = { workspace = true } serde = { workspace = true } uuid = { workspace = true } rand = { workspace = true } diff --git a/atuin-common/src/api.rs b/atuin-common/src/api.rs index 5622bd8..ddcc0b0 100644 --- a/atuin-common/src/api.rs +++ b/atuin-common/src/api.rs @@ -1,6 +1,6 @@ -use chrono::Utc; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use time::OffsetDateTime; #[derive(Debug, Serialize, Deserialize)] pub struct UserResponse { @@ -36,7 +36,8 @@ pub struct LoginResponse { #[derive(Debug, Serialize, Deserialize)] pub struct AddHistoryRequest { pub id: String, - pub timestamp: chrono::DateTime, + #[serde(with = "time::serde::rfc3339")] + pub timestamp: OffsetDateTime, pub data: String, pub hostname: String, } @@ -48,8 +49,10 @@ pub struct CountResponse { #[derive(Debug, Serialize, Deserialize)] pub struct SyncHistoryRequest { - pub sync_ts: chrono::DateTime, - pub history_ts: chrono::DateTime, + #[serde(with = "time::serde::rfc3339")] + pub sync_ts: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub history_ts: OffsetDateTime, pub host: String, } diff --git a/atuin-common/src/record.rs b/atuin-common/src/record.rs index b00c03c..cba0917 100644 --- a/atuin-common/src/record.rs +++ b/atuin-common/src/record.rs @@ -42,7 +42,7 @@ pub struct Record { pub parent: Option, /// The creation time in nanoseconds since unix epoch - #[builder(default = chrono::Utc::now().timestamp_nanos() as u64)] + #[builder(default = time::OffsetDateTime::now_utc().unix_timestamp_nanos() as u64)] pub timestamp: u64, /// The version the data in the entry conforms to diff --git a/atuin-common/src/utils.rs b/atuin-common/src/utils.rs index ed98e27..7cf4e9d 100644 --- a/atuin-common/src/utils.rs +++ b/atuin-common/src/utils.rs @@ -1,7 +1,6 @@ use std::env; use std::path::PathBuf; -use chrono::{Months, NaiveDate}; use rand::RngCore; use uuid::Uuid; @@ -37,7 +36,10 @@ const fn encode_unix_timestamp_millis(millis: u64, random_bytes: &[u8; 10]) -> U pub fn uuid_v7() -> Uuid { let bytes = random_bytes(); - let now: u64 = chrono::Utc::now().timestamp_millis() as u64; + let now: u64 = u64::try_from( + time::OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, + ) + .expect("Either you're in the past (1970) - or your in the far future (2554). Good for you"); encode_unix_timestamp_millis(now, &bytes) } @@ -111,18 +113,10 @@ pub fn get_current_dir() -> String { } } -pub fn get_days_from_month(year: i32, month: u32) -> i64 { - let Some(start) = NaiveDate::from_ymd_opt(year, month, 1) else { - return 30; - }; - let Some(end) = start.checked_add_months(Months::new(1)) else { - return 30; - }; - end.signed_duration_since(start).num_days() -} - #[cfg(test)] mod tests { + use time::Month; + use super::*; use std::env; @@ -170,21 +164,21 @@ mod tests { #[test] fn days_from_month() { - assert_eq!(get_days_from_month(2023, 1), 31); - assert_eq!(get_days_from_month(2023, 2), 28); - assert_eq!(get_days_from_month(2023, 3), 31); - assert_eq!(get_days_from_month(2023, 4), 30); - assert_eq!(get_days_from_month(2023, 5), 31); - assert_eq!(get_days_from_month(2023, 6), 30); - assert_eq!(get_days_from_month(2023, 7), 31); - assert_eq!(get_days_from_month(2023, 8), 31); - assert_eq!(get_days_from_month(2023, 9), 30); - assert_eq!(get_days_from_month(2023, 10), 31); - assert_eq!(get_days_from_month(2023, 11), 30); - assert_eq!(get_days_from_month(2023, 12), 31); + assert_eq!(time::util::days_in_year_month(2023, Month::January), 31); + assert_eq!(time::util::days_in_year_month(2023, Month::February), 28); + assert_eq!(time::util::days_in_year_month(2023, Month::March), 31); + assert_eq!(time::util::days_in_year_month(2023, Month::April), 30); + assert_eq!(time::util::days_in_year_month(2023, Month::May), 31); + assert_eq!(time::util::days_in_year_month(2023, Month::June), 30); + assert_eq!(time::util::days_in_year_month(2023, Month::July), 31); + assert_eq!(time::util::days_in_year_month(2023, Month::August), 31); + assert_eq!(time::util::days_in_year_month(2023, Month::September), 30); + assert_eq!(time::util::days_in_year_month(2023, Month::October), 31); + assert_eq!(time::util::days_in_year_month(2023, Month::November), 30); + assert_eq!(time::util::days_in_year_month(2023, Month::December), 31); // leap years - assert_eq!(get_days_from_month(2024, 2), 29); + assert_eq!(time::util::days_in_year_month(2024, Month::February), 29); } #[test] diff --git a/atuin-server-database/Cargo.toml b/atuin-server-database/Cargo.toml index 8f1f775..1b416f9 100644 --- a/atuin-server-database/Cargo.toml +++ b/atuin-server-database/Cargo.toml @@ -13,9 +13,8 @@ repository = { workspace = true } atuin-common = { path = "../atuin-common", version = "16.0.0" } tracing = "0.1" -chrono = { workspace = true } +time = { workspace = true } eyre = { workspace = true } uuid = { workspace = true } serde = { workspace = true } async-trait = { workspace = true } -chronoutil = "0.2.3" diff --git a/atuin-server-database/src/lib.rs b/atuin-server-database/src/lib.rs index 06ecf91..4ebd517 100644 --- a/atuin-server-database/src/lib.rs +++ b/atuin-server-database/src/lib.rs @@ -13,13 +13,9 @@ use self::{ models::{History, NewHistory, NewSession, NewUser, Session, User}, }; use async_trait::async_trait; -use atuin_common::{ - record::{EncryptedData, HostId, Record, RecordId, RecordIndex}, - utils::get_days_from_month, -}; -use chrono::{Datelike, TimeZone}; -use chronoutil::RelativeDuration; +use atuin_common::record::{EncryptedData, HostId, Record, RecordId, RecordIndex}; use serde::{de::DeserializeOwned, Serialize}; +use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Time}; use tracing::instrument; #[derive(Debug)] @@ -34,6 +30,12 @@ impl Display for DbError { } } +impl> From for DbError { + fn from(value: T) -> Self { + DbError::Other(value.into().into()) + } +} + impl std::error::Error for DbError {} pub type DbResult = Result; @@ -75,15 +77,15 @@ pub trait Database: Sized + Clone + Send + Sync + 'static { async fn count_history_range( &self, user: &User, - start: chrono::NaiveDateTime, - end: chrono::NaiveDateTime, + start: PrimitiveDateTime, + end: PrimitiveDateTime, ) -> DbResult; async fn list_history( &self, user: &User, - created_after: chrono::NaiveDateTime, - since: chrono::NaiveDateTime, + created_after: OffsetDateTime, + since: OffsetDateTime, host: &str, page_size: i64, ) -> DbResult>; @@ -95,53 +97,51 @@ pub trait Database: Sized + Clone + Send + Sync + 'static { /// Count the history for a given year #[instrument(skip_all)] async fn count_history_year(&self, user: &User, year: i32) -> DbResult { - let start = chrono::Utc.ymd(year, 1, 1).and_hms_nano(0, 0, 0, 0); - let end = start + RelativeDuration::years(1); + let start = Date::from_calendar_date(year, time::Month::January, 1)?; + let end = Date::from_calendar_date(year + 1, time::Month::January, 1)?; let res = self - .count_history_range(user, start.naive_utc(), end.naive_utc()) + .count_history_range( + user, + start.with_time(Time::MIDNIGHT), + end.with_time(Time::MIDNIGHT), + ) .await?; Ok(res) } /// Count the history for a given month #[instrument(skip_all)] - async fn count_history_month(&self, user: &User, month: chrono::NaiveDate) -> DbResult { - let start = chrono::Utc - .ymd(month.year(), month.month(), 1) - .and_hms_nano(0, 0, 0, 0); - - // ofc... - let end = if month.month() < 12 { - chrono::Utc - .ymd(month.year(), month.month() + 1, 1) - .and_hms_nano(0, 0, 0, 0) - } else { - chrono::Utc - .ymd(month.year() + 1, 1, 1) - .and_hms_nano(0, 0, 0, 0) - }; + async fn count_history_month(&self, user: &User, year: i32, month: Month) -> DbResult { + let start = Date::from_calendar_date(year, month, 1)?; + let days = time::util::days_in_year_month(year, month); + let end = start + Duration::days(days as i64); tracing::debug!("start: {}, end: {}", start, end); let res = self - .count_history_range(user, start.naive_utc(), end.naive_utc()) + .count_history_range( + user, + start.with_time(Time::MIDNIGHT), + end.with_time(Time::MIDNIGHT), + ) .await?; Ok(res) } /// Count the history for a given day #[instrument(skip_all)] - async fn count_history_day(&self, user: &User, day: chrono::NaiveDate) -> DbResult { - let start = chrono::Utc - .ymd(day.year(), day.month(), day.day()) - .and_hms_nano(0, 0, 0, 0); - let end = chrono::Utc - .ymd(day.year(), day.month(), day.day() + 1) - .and_hms_nano(0, 0, 0, 0); + async fn count_history_day(&self, user: &User, day: Date) -> DbResult { + let end = day + .next_day() + .ok_or_else(|| DbError::Other(eyre::eyre!("no next day?")))?; let res = self - .count_history_range(user, start.naive_utc(), end.naive_utc()) + .count_history_range( + user, + day.with_time(Time::MIDNIGHT), + end.with_time(Time::MIDNIGHT), + ) .await?; Ok(res) } @@ -152,7 +152,7 @@ pub trait Database: Sized + Clone + Send + Sync + 'static { user: &User, period: TimePeriod, year: u64, - month: u64, + month: Month, ) -> DbResult> { // TODO: Support different timezones. Right now we assume UTC and // everything is stored as such. But it _should_ be possible to @@ -164,7 +164,7 @@ pub trait Database: Sized + Clone + Send + Sync + 'static { // First we need to work out how far back to calculate. Get the // oldest history item let oldest = self.oldest_history(user).await?.timestamp.year(); - let current_year = chrono::Utc::now().year(); + let current_year = OffsetDateTime::now_utc().year(); // All the years we need to get data for // The upper bound is exclusive, so include current +1 @@ -188,13 +188,10 @@ pub trait Database: Sized + Clone + Send + Sync + 'static { TimePeriod::MONTH => { let mut ret = HashMap::new(); - for month in 1..13 { - let count = self - .count_history_month( - user, - chrono::Utc.ymd(year as i32, month, 1).naive_utc(), - ) - .await?; + let months = + std::iter::successors(Some(Month::January), |m| Some(m.next())).take(12); + for month in months { + let count = self.count_history_month(user, year as i32, month).await?; ret.insert( month as u64, @@ -211,14 +208,9 @@ pub trait Database: Sized + Clone + Send + Sync + 'static { TimePeriod::DAY => { let mut ret = HashMap::new(); - for day in 1..get_days_from_month(year as i32, month as u32) { + for day in 1..time::util::days_in_year_month(year as i32, month) { let count = self - .count_history_day( - user, - chrono::Utc - .ymd(year as i32, month as u32, day as u32) - .naive_utc(), - ) + .count_history_day(user, Date::from_calendar_date(year as i32, month, day)?) .await?; ret.insert( diff --git a/atuin-server-database/src/models.rs b/atuin-server-database/src/models.rs index 7183b1e..b71a9bc 100644 --- a/atuin-server-database/src/models.rs +++ b/atuin-server-database/src/models.rs @@ -1,25 +1,25 @@ -use chrono::prelude::*; +use time::OffsetDateTime; pub struct History { pub id: i64, pub client_id: String, // a client generated ID pub user_id: i64, pub hostname: String, - pub timestamp: NaiveDateTime, + pub timestamp: OffsetDateTime, /// All the data we have about this command, encrypted. /// /// Currently this is an encrypted msgpack object, but this may change in the future. pub data: String, - pub created_at: NaiveDateTime, + pub created_at: OffsetDateTime, } pub struct NewHistory { pub client_id: String, pub user_id: i64, pub hostname: String, - pub timestamp: chrono::NaiveDateTime, + pub timestamp: OffsetDateTime, /// All the data we have about this command, encrypted. /// diff --git a/atuin-server-postgres/Cargo.toml b/atuin-server-postgres/Cargo.toml index f490a97..703e121 100644 --- a/atuin-server-postgres/Cargo.toml +++ b/atuin-server-postgres/Cargo.toml @@ -14,7 +14,7 @@ atuin-common = { path = "../atuin-common", version = "16.0.0" } atuin-server-database = { path = "../atuin-server-database", version = "16.0.0" } tracing = "0.1" -chrono = { workspace = true } +time = { workspace = true } serde = { workspace = true } sqlx = { workspace = true } async-trait = { workspace = true } diff --git a/atuin-server-postgres/src/lib.rs b/atuin-server-postgres/src/lib.rs index aa52322..8f473d5 100644 --- a/atuin-server-postgres/src/lib.rs +++ b/atuin-server-postgres/src/lib.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPoolOptions; use sqlx::Row; +use time::{OffsetDateTime, PrimitiveDateTime}; use tracing::instrument; use wrappers::{DbHistory, DbRecord, DbSession, DbUser}; @@ -139,7 +140,7 @@ impl Database for Postgres { ) .bind(user.id) .bind(id) - .bind(chrono::Utc::now().naive_utc()) + .bind(OffsetDateTime::now_utc()) .fetch_all(&self.pool) .await .map_err(fix_error)?; @@ -175,8 +176,8 @@ impl Database for Postgres { async fn count_history_range( &self, user: &User, - start: chrono::NaiveDateTime, - end: chrono::NaiveDateTime, + start: PrimitiveDateTime, + end: PrimitiveDateTime, ) -> DbResult { let res: (i64,) = sqlx::query_as( "select count(1) from history @@ -198,8 +199,8 @@ impl Database for Postgres { async fn list_history( &self, user: &User, - created_after: chrono::NaiveDateTime, - since: chrono::NaiveDateTime, + created_after: OffsetDateTime, + since: OffsetDateTime, host: &str, page_size: i64, ) -> DbResult> { diff --git a/atuin-server/Cargo.toml b/atuin-server/Cargo.toml index f1c44b4..1b9ad85 100644 --- a/atuin-server/Cargo.toml +++ b/atuin-server/Cargo.toml @@ -3,6 +3,7 @@ name = "atuin-server" edition = "2018" description = "server library for atuin" +rust-version = { workspace = true } version = { workspace = true } authors = { workspace = true } license = { workspace = true } @@ -14,7 +15,7 @@ atuin-common = { path = "../atuin-common", version = "16.0.0" } atuin-server-database = { path = "../atuin-server-database", version = "16.0.0" } tracing = "0.1" -chrono = { workspace = true } +time = { workspace = true } eyre = { workspace = true } uuid = { workspace = true } config = { workspace = true } @@ -27,7 +28,6 @@ async-trait = { workspace = true } axum = "0.6.4" http = "0.2" fs-err = { workspace = true } -chronoutil = "0.2.3" tower = "0.4" tower-http = { version = "0.3", features = ["trace"] } reqwest = { workspace = true } diff --git a/atuin-server/src/handlers/history.rs b/atuin-server/src/handlers/history.rs index bb0aa32..263d6cb 100644 --- a/atuin-server/src/handlers/history.rs +++ b/atuin-server/src/handlers/history.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, convert::TryFrom}; use axum::{ extract::{Path, Query, State}, @@ -6,6 +6,7 @@ use axum::{ Json, }; use http::StatusCode; +use time::Month; use tracing::{debug, error, instrument}; use super::{ErrorResponse, ErrorResponseStatus, RespExt}; @@ -63,16 +64,10 @@ pub async fn list( }; let history = db - .list_history( - &user, - req.sync_ts.naive_utc(), - req.history_ts.naive_utc(), - &req.host, - page_size, - ) + .list_history(&user, req.sync_ts, req.history_ts, &req.host, page_size) .await; - if req.sync_ts.timestamp_nanos() < 0 || req.history_ts.timestamp_nanos() < 0 { + if req.sync_ts.unix_timestamp_nanos() < 0 || req.history_ts.unix_timestamp_nanos() < 0 { error!("client asked for history from < epoch 0"); return Err( ErrorResponse::reply("asked for history from before epoch 0") @@ -139,7 +134,7 @@ pub async fn add( client_id: h.id, user_id: user.id, hostname: h.hostname, - timestamp: h.timestamp.naive_utc(), + timestamp: h.timestamp, data: h.data, }) .collect(); @@ -182,11 +177,17 @@ pub async fn calendar( let year = params.get("year").unwrap_or(&0); let month = params.get("month").unwrap_or(&1); + let month = Month::try_from(*month as u8).map_err(|e| ErrorResponseStatus { + error: ErrorResponse { + reason: e.to_string().into(), + }, + status: http::StatusCode::BAD_REQUEST, + })?; let db = &state.0.database; let focus = match focus { "year" => db - .calendar(&user, TimePeriod::YEAR, *year, *month) + .calendar(&user, TimePeriod::YEAR, *year, month) .await .map_err(|_| { ErrorResponse::reply("failed to query calendar") @@ -194,7 +195,7 @@ pub async fn calendar( }), "month" => db - .calendar(&user, TimePeriod::MONTH, *year, *month) + .calendar(&user, TimePeriod::MONTH, *year, month) .await .map_err(|_| { ErrorResponse::reply("failed to query calendar") @@ -202,7 +203,7 @@ pub async fn calendar( }), "day" => db - .calendar(&user, TimePeriod::DAY, *year, *month) + .calendar(&user, TimePeriod::DAY, *year, month) .await .map_err(|_| { ErrorResponse::reply("failed to query calendar") diff --git a/atuin/Cargo.toml b/atuin/Cargo.toml index 5c09689..ec5b1c5 100644 --- a/atuin/Cargo.toml +++ b/atuin/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "atuin" edition = "2021" -rust-version = "1.59" description = "atuin - magical shell history" readme = "./README.md" +rust-version = { workspace = true } version = { workspace = true } authors = { workspace = true } license = { workspace = true } @@ -46,7 +46,7 @@ atuin-common = { path = "../atuin-common", version = "16.0.0" } log = { workspace = true } env_logger = "0.10.0" -chrono = { version = "0.4", features = ["serde"] } +time = { workspace = true } eyre = { workspace = true } directories = { workspace = true } indicatif = "0.17.5" diff --git a/atuin/src/command/client/history.rs b/atuin/src/command/client/history.rs index 44e621e..b33591b 100644 --- a/atuin/src/command/client/history.rs +++ b/atuin/src/command/client/history.rs @@ -7,7 +7,7 @@ use std::{ use atuin_common::utils; use clap::Subcommand; -use eyre::Result; +use eyre::{Context, Result}; use runtime_format::{FormatKey, FormatKeyError, ParseSegment, ParsedFmt}; use atuin_client::{ @@ -19,8 +19,8 @@ use atuin_client::{ #[cfg(feature = "sync")] use atuin_client::sync; use log::debug; +use time::{macros::format_description, OffsetDateTime}; -use super::search::format_duration; use super::search::format_duration_into; #[derive(Subcommand)] @@ -141,6 +141,9 @@ pub fn print_list(h: &[History], list_mode: ListMode, format: Option<&str>) { /// type wrapper around `History` so we can implement traits struct FmtHistory<'a>(&'a History); +static TIME_FMT: &[time::format_description::FormatItem<'static>] = + format_description!("[year]-[month]-[day] [hour repr:24]:[minute]:[second]"); + /// defines how to format the history impl FormatKey for FmtHistory<'_> { #[allow(clippy::cast_sign_loss)] @@ -153,11 +156,17 @@ impl FormatKey for FmtHistory<'_> { let dur = Duration::from_nanos(std::cmp::max(self.0.duration, 0) as u64); format_duration_into(dur, f)?; } - "time" => self.0.timestamp.format("%Y-%m-%d %H:%M:%S").fmt(f)?, + "time" => { + self.0 + .timestamp + .format(TIME_FMT) + .map_err(|_| fmt::Error)? + .fmt(f)?; + } "relativetime" => { - let since = chrono::Utc::now() - self.0.timestamp; - let time = format_duration(since.to_std().unwrap_or_default()); - f.write_str(&time)?; + let since = OffsetDateTime::now_utc() - self.0.timestamp; + let d = Duration::try_from(since).unwrap_or_default(); + format_duration_into(d, f)?; } "host" => f.write_str( self.0 @@ -184,6 +193,7 @@ fn parse_fmt(format: &str) -> ParsedFmt { } impl Cmd { + #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)] async fn handle_start( db: &mut impl Database, settings: &Settings, @@ -196,7 +206,7 @@ impl Cmd { let cwd = utils::get_current_dir(); let h: History = History::capture() - .timestamp(chrono::Utc::now()) + .timestamp(OffsetDateTime::now_utc()) .command(command) .cwd(cwd) .build() @@ -233,7 +243,8 @@ impl Cmd { } h.exit = exit; - h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp.timestamp_nanos(); + h.duration = i64::try_from((OffsetDateTime::now_utc() - h.timestamp).whole_nanoseconds()) + .context("command took over 292 years")?; db.update(&h).await?; diff --git a/atuin/src/command/client/search.rs b/atuin/src/command/client/search.rs index 95e92fd..bdfa30b 100644 --- a/atuin/src/command/client/search.rs +++ b/atuin/src/command/client/search.rs @@ -178,7 +178,7 @@ impl Cmd { // This is supposed to more-or-less mirror the command line version, so ofc // it is going to have a lot of args -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::cast_possible_truncation)] async fn run_non_interactive( settings: &Settings, filter_options: OptFilters, diff --git a/atuin/src/command/client/search/engines/skim.rs b/atuin/src/command/client/search/engines/skim.rs index 375a018..06b717e 100644 --- a/atuin/src/command/client/search/engines/skim.rs +++ b/atuin/src/command/client/search/engines/skim.rs @@ -2,10 +2,10 @@ use std::path::Path; use async_trait::async_trait; use atuin_client::{database::Database, history::History, settings::FilterMode}; -use chrono::Utc; use eyre::Result; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use itertools::Itertools; +use time::OffsetDateTime; use tokio::task::yield_now; use super::{SearchEngine, SearchState}; @@ -47,7 +47,7 @@ async fn fuzzy_search( let mut set = Vec::with_capacity(200); let mut ranks = Vec::with_capacity(200); let query = state.input.as_str(); - let now = Utc::now(); + let now = OffsetDateTime::now_utc(); for (i, (history, count)) in all_history.iter().enumerate() { if i % 256 == 0 { @@ -78,7 +78,7 @@ async fn fuzzy_search( if let Some((score, indices)) = engine.fuzzy_indices(&history.command, query) { let begin = indices.first().copied().unwrap_or_default(); - let mut duration = ((now - history.timestamp).num_seconds() as f64).log2(); + let mut duration = (now - history.timestamp).as_seconds_f64().log2(); if !duration.is_finite() || duration <= 1.0 { duration = 1.0; } diff --git a/atuin/src/command/client/search/history_list.rs b/atuin/src/command/client/search/history_list.rs index 64c6ccc..ed23f63 100644 --- a/atuin/src/command/client/search/history_list.rs +++ b/atuin/src/command/client/search/history_list.rs @@ -7,6 +7,7 @@ use ratatui::{ style::{Color, Modifier, Style}, widgets::{Block, StatefulWidget, Widget}, }; +use time::OffsetDateTime; use super::format_duration; @@ -151,8 +152,8 @@ impl DrawState<'_> { // would fail. // If the timestamp would otherwise be in the future, display // the time since as 0. - let since = chrono::Utc::now() - h.timestamp; - let time = format_duration(since.to_std().unwrap_or_default()); + let since = OffsetDateTime::now_utc() - h.timestamp; + let time = format_duration(since.try_into().unwrap_or_default()); // pad the time a little bit before we write. this aligns things nicely self.x = PREFIX_LENGTH - 4 - time.len() as u16; diff --git a/atuin/src/command/client/stats.rs b/atuin/src/command/client/stats.rs index cbb5682..1293939 100644 --- a/atuin/src/command/client/stats.rs +++ b/atuin/src/command/client/stats.rs @@ -1,6 +1,5 @@ use std::collections::{HashMap, HashSet}; -use chrono::{prelude::*, Duration}; use clap::Parser; use crossterm::style::{Color, ResetColor, SetAttribute, SetForegroundColor}; use eyre::{bail, Result}; @@ -11,6 +10,7 @@ use atuin_client::{ history::History, settings::{FilterMode, Settings}, }; +use time::{Duration, OffsetDateTime, Time}; #[derive(Parser)] #[command(infer_subcommands = true)] @@ -84,25 +84,29 @@ impl Cmd { let history = if words.as_str() == "all" { db.list(FilterMode::Global, &context, None, false).await? } else if words.trim() == "today" { - let start = Local::now().date().and_hms(0, 0, 0); + let start = OffsetDateTime::now_local()?.replace_time(Time::MIDNIGHT); let end = start + Duration::days(1); - db.range(start.into(), end.into()).await? + db.range(start, end).await? } else if words.trim() == "month" { - let end = Local::now().date().and_hms(0, 0, 0); + let end = OffsetDateTime::now_local()?.replace_time(Time::MIDNIGHT); let start = end - Duration::days(31); - db.range(start.into(), end.into()).await? + db.range(start, end).await? } else if words.trim() == "week" { - let end = Local::now().date().and_hms(0, 0, 0); + let end = OffsetDateTime::now_local()?.replace_time(Time::MIDNIGHT); let start = end - Duration::days(7); - db.range(start.into(), end.into()).await? + db.range(start, end).await? } else if words.trim() == "year" { - let end = Local::now().date().and_hms(0, 0, 0); + let end = OffsetDateTime::now_local()?.replace_time(Time::MIDNIGHT); let start = end - Duration::days(365); - db.range(start.into(), end.into()).await? + db.range(start, end).await? } else { - let start = parse_date_string(&words, Local::now(), settings.dialect.into())?; + let start = parse_date_string( + &words, + OffsetDateTime::now_local()?, + settings.dialect.into(), + )?; let end = start + Duration::days(1); - db.range(start.into(), end.into()).await? + db.range(start, end).await? }; compute_stats(&history, self.count)?; Ok(()) diff --git a/deny.toml b/deny.toml index 94ae5c7..c82e664 100644 --- a/deny.toml +++ b/deny.toml @@ -26,8 +26,6 @@ unmaintained = "warn" yanked = "warn" notice = "warn" ignore = [ - # time 0.1 - code path not taken - "RUSTSEC-2020-0071", # potential to misuse ed25519-dalek 1.0 # used by rusty-paseto. not in a vulnerable way # and we don't even use paseto public key crypto so we don't use this