aboutsummaryrefslogtreecommitdiffstats
path: root/crates/turtle/src/atuin_client
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-13 00:50:54 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-06-13 00:50:54 +0200
commit6723829a3398b3c9dd6dc6ae79124f46000606ee (patch)
treea1ec535eddd711a4557e4bcc5b94382c3623504c /crates/turtle/src/atuin_client
parentchore(treewide): Cleanup themes (diff)
downloadatuin-6723829a3398b3c9dd6dc6ae79124f46000606ee.zip
chore(treewide): Remove `cargo` warnings to 0
There are still the `clippy` warnings, but they are for a future date.
Diffstat (limited to 'crates/turtle/src/atuin_client')
-rw-r--r--crates/turtle/src/atuin_client/api_client.rs16
-rw-r--r--crates/turtle/src/atuin_client/encryption.rs4
-rw-r--r--crates/turtle/src/atuin_client/history.rs6
-rw-r--r--crates/turtle/src/atuin_client/history/store.rs78
-rw-r--r--crates/turtle/src/atuin_client/record/encryption.rs22
-rw-r--r--crates/turtle/src/atuin_client/record/sqlite_store.rs100
-rw-r--r--crates/turtle/src/atuin_client/record/sync.rs219
-rw-r--r--crates/turtle/src/atuin_client/secrets.rs2
-rw-r--r--crates/turtle/src/atuin_client/settings.rs68
-rw-r--r--crates/turtle/src/atuin_client/settings/watcher.rs4
10 files changed, 118 insertions, 401 deletions
diff --git a/crates/turtle/src/atuin_client/api_client.rs b/crates/turtle/src/atuin_client/api_client.rs
index 15d96d93..bd5bf59e 100644
--- a/crates/turtle/src/atuin_client/api_client.rs
+++ b/crates/turtle/src/atuin_client/api_client.rs
@@ -19,8 +19,8 @@ static APP_USER_AGENT: &str = concat!("atuin/", env!("CARGO_PKG_VERSION"),);
pub(crate) struct Client<'a> {
sync_addr: &'a str,
- client: reqwest::Client,
user_id: Uuid,
+ inner: reqwest::Client,
}
fn make_url(address: &str, path: &str, user_id: Uuid) -> Result<String> {
@@ -28,7 +28,7 @@ fn make_url(address: &str, path: &str, user_id: Uuid) -> Result<String> {
// `join()` expects a trailing `/` in order to join paths
// e.g. it treats `http://host:port/subdir` as a file called `subdir`
- let address = &format!("{address}/api/v0/{}/", user_id.to_string());
+ let address = &format!("{address}/api/v0/{user_id}/");
// passing a path with a leading `/` will cause `join()` to replace the entire URL path
let path = path.strip_prefix("/").unwrap_or(path);
@@ -81,7 +81,7 @@ async fn handle_resp_error(resp: Response) -> Result<Response> {
}
if !status.is_success() {
- if let Ok(error) = resp.json::<ErrorResponse>().await {
+ if let Ok(error) = resp.json::<ErrorResponse<'_>>().await {
let reason = error.reason;
if status.is_client_error() {
@@ -117,7 +117,7 @@ impl<'a> Client<'a> {
Ok(Client {
user_id,
sync_addr,
- client: reqwest::Client::builder()
+ inner: reqwest::Client::builder()
.user_agent(APP_USER_AGENT)
.default_headers(headers)
.connect_timeout(Duration::new(connect_timeout, 0))
@@ -130,7 +130,7 @@ impl<'a> Client<'a> {
let url = make_url(self.sync_addr, "/store", self.user_id)?;
let url = Url::parse(url.as_str())?;
- let resp = self.client.delete(url).send().await?;
+ let resp = self.inner.delete(url).send().await?;
handle_resp_error(resp).await?;
@@ -143,7 +143,7 @@ impl<'a> Client<'a> {
debug!("uploading {} records to {url}", records.len());
- let resp = self.client.post(url).json(records).send().await?;
+ let resp = self.inner.post(url).json(records).send().await?;
handle_resp_error(resp).await?;
Ok(())
@@ -169,7 +169,7 @@ impl<'a> Client<'a> {
let url = Url::parse(url.as_str())?;
- let resp = self.client.get(url).send().await?;
+ let resp = self.inner.get(url).send().await?;
let resp = handle_resp_error(resp).await?;
let records = resp.json::<Vec<Record<EncryptedData>>>().await?;
@@ -181,7 +181,7 @@ impl<'a> Client<'a> {
let url = make_url(self.sync_addr, "/record", self.user_id)?;
let url = Url::parse(url.as_str())?;
- let resp = self.client.get(url).send().await?;
+ let resp = self.inner.get(url).send().await?;
let resp = handle_resp_error(resp).await?;
if !ensure_version(&resp)? {
diff --git a/crates/turtle/src/atuin_client/encryption.rs b/crates/turtle/src/atuin_client/encryption.rs
index e9c8d7e9..661a6669 100644
--- a/crates/turtle/src/atuin_client/encryption.rs
+++ b/crates/turtle/src/atuin_client/encryption.rs
@@ -75,7 +75,7 @@ pub(crate) fn decode_key(key: String) -> Result<Key> {
if let Ok(key) = <[u8; 32]>::try_from(&*buf) {
Ok(key.into())
} else {
- let mut bytes = rmp::decode::Bytes::new(&buf);
+ let mut bytes = decode::Bytes::new(&buf);
match Marker::from_u8(buf[0]) {
Marker::Bin8 => {
@@ -91,7 +91,7 @@ pub(crate) fn decode_key(key: String) -> Result<Key> {
let mut key = Key::default();
for i in &mut key {
- *i = rmp::decode::read_int(&mut bytes).map_err(|err| eyre!("{err:?}"))?;
+ *i = decode::read_int(&mut bytes).map_err(|err| eyre!("{err:?}"))?;
}
Ok(key)
}
diff --git a/crates/turtle/src/atuin_client/history.rs b/crates/turtle/src/atuin_client/history.rs
index 1f89cd71..11d60548 100644
--- a/crates/turtle/src/atuin_client/history.rs
+++ b/crates/turtle/src/atuin_client/history.rs
@@ -272,7 +272,7 @@ impl History {
Ok(History {
id: id.to_owned().into(),
- timestamp: OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)?,
+ timestamp: OffsetDateTime::from_unix_timestamp_nanos(i128::from(timestamp))?,
duration,
exit,
command: command.to_owned(),
@@ -282,7 +282,7 @@ impl History {
author: Self::author_from_hostname(hostname),
intent: None,
deleted_at: deleted_at
- .map(|t| OffsetDateTime::from_unix_timestamp_nanos(t as i128))
+ .map(|t| OffsetDateTime::from_unix_timestamp_nanos(i128::from(t)))
.transpose()?,
})
}
@@ -553,7 +553,7 @@ mod tests {
fn disable_secrets() {
let settings = Settings {
secrets_filter: false,
- ..Settings::utc()
+ ..Settings::new().unwrap()
};
let stripe_key: History = History::capture()
diff --git a/crates/turtle/src/atuin_client/history/store.rs b/crates/turtle/src/atuin_client/history/store.rs
index c6e079f3..9c7771cc 100644
--- a/crates/turtle/src/atuin_client/history/store.rs
+++ b/crates/turtle/src/atuin_client/history/store.rs
@@ -27,12 +27,12 @@ pub(crate) enum HistoryRecord {
}
impl HistoryRecord {
- /// Serialize a history record, returning DecryptedData
+ /// Serialize a history record, returning `DecryptedData`
/// The record will be of a certain type
/// We map those like so:
///
- /// HistoryRecord::Create -> 0
- /// HistoryRecord::Delete-> 1
+ /// `HistoryRecord::Create` -> 0
+ /// `HistoryRecord::Delete`-> 1
///
/// This numeric identifier is then written as the first byte to the buffer. For history, we
/// append the serialized history right afterwards, to avoid having to handle serialization
@@ -47,7 +47,7 @@ impl HistoryRecord {
let mut output = vec![];
match self {
- HistoryRecord::Create(history) => {
+ Self::Create(history) => {
// 0 -> a history create
encode::write_u8(&mut output, 0)?;
@@ -55,12 +55,12 @@ impl HistoryRecord {
encode::write_bin(&mut output, &bytes.0)?;
}
- HistoryRecord::Delete(id) => {
+ Self::Delete(id) => {
// 1 -> a history delete
encode::write_u8(&mut output, 1)?;
encode::write_str(&mut output, id.0.as_str())?;
}
- };
+ }
Ok(DecryptedData(output))
}
@@ -81,11 +81,11 @@ impl HistoryRecord {
0 => {
// not super useful to us atm, but perhaps in the future
// written by write_bin above
- let _ = decode::read_bin_len(&mut bytes).map_err(error_report)?;
+ decode::read_bin_len(&mut bytes).map_err(error_report)?;
let record = History::deserialize(bytes.remaining_slice(), version)?;
- Ok(HistoryRecord::Create(record))
+ Ok(Self::Create(record))
}
// 1 -> HistoryRecord::Delete
@@ -99,7 +99,7 @@ impl HistoryRecord {
);
}
- Ok(HistoryRecord::Delete(id.to_string().into()))
+ Ok(Self::Delete(id.to_string().into()))
}
n => {
@@ -111,7 +111,7 @@ impl HistoryRecord {
impl HistoryStore {
pub(crate) fn new(store: SqliteStore, host_id: HostId, encryption_key: [u8; 32]) -> Self {
- HistoryStore {
+ Self {
store,
host_id,
encryption_key,
@@ -182,7 +182,7 @@ impl HistoryStore {
}
/// Delete a batch of history entries via the record store.
- /// Returns the record IDs so the caller can run incremental_build when ready.
+ /// Returns the record IDs so the caller can run `incremental_build` when ready.
pub(crate) async fn delete_entries(
&self,
entries: impl IntoIterator<Item = History>,
@@ -209,7 +209,7 @@ impl HistoryStore {
let records = self.store.all_tagged(HISTORY_TAG).await?;
let mut ret = Vec::with_capacity(records.len());
- for record in records.into_iter() {
+ for record in records {
let hist = match record.version.as_str() {
HISTORY_VERSION_V0 | HISTORY_VERSION => {
let version = record.version.clone();
@@ -266,33 +266,28 @@ impl HistoryStore {
for id in ids {
let record = self.store.get(*id).await;
- let record = match record {
- Ok(record) => record,
- _ => {
+ if let Ok(record) = record {
+ if record.tag != HISTORY_TAG {
continue;
}
- };
- if record.tag != HISTORY_TAG {
- continue;
- }
+ let version = record.version.clone();
+ let decrypted = record.decrypt::<PASETO_V4>(&self.encryption_key)?;
+ let record = match version.as_str() {
+ HISTORY_VERSION_V0 | HISTORY_VERSION => {
+ HistoryRecord::deserialize(&decrypted.data, version.as_str())?
+ }
+ version => bail!("unknown history version {version:?}"),
+ };
- let version = record.version.clone();
- let decrypted = record.decrypt::<PASETO_V4>(&self.encryption_key)?;
- let record = match version.as_str() {
- HISTORY_VERSION_V0 | HISTORY_VERSION => {
- HistoryRecord::deserialize(&decrypted.data, version.as_str())?
- }
- version => bail!("unknown history version {version:?}"),
- };
-
- match record {
- HistoryRecord::Create(h) => {
- // TODO: benchmark CPU time/memory tradeoff of batch commit vs one at a time
- database.save(&h).await?;
- }
- HistoryRecord::Delete(id) => {
- database.delete_rows(&[id]).await?;
+ match record {
+ HistoryRecord::Create(h) => {
+ // TODO: benchmark CPU time/memory tradeoff of batch commit vs one at a time
+ database.save(&h).await?;
+ }
+ HistoryRecord::Delete(id) => {
+ database.delete_rows(&[id]).await?;
+ }
}
}
}
@@ -306,10 +301,13 @@ impl HistoryStore {
pub(crate) async fn history_ids(&self) -> Result<HashSet<HistoryId>> {
let history = self.history().await?;
- let ret = HashSet::from_iter(history.iter().map(|h| match h {
- HistoryRecord::Create(h) => h.id.clone(),
- HistoryRecord::Delete(id) => id.clone(),
- }));
+ let ret = history
+ .iter()
+ .map(|h| match h {
+ HistoryRecord::Create(h) => h.id.clone(),
+ HistoryRecord::Delete(id) => id.clone(),
+ })
+ .collect::<HashSet<_>>();
Ok(ret)
}
@@ -320,7 +318,7 @@ impl HistoryStore {
ProgressStyle::with_template("{spinner:.blue} {msg}")
.unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn Write| {
- write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
+ write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap();
})
.progress_chars("#>-"),
);
diff --git a/crates/turtle/src/atuin_client/record/encryption.rs b/crates/turtle/src/atuin_client/record/encryption.rs
index 70723bb7..afac5b02 100644
--- a/crates/turtle/src/atuin_client/record/encryption.rs
+++ b/crates/turtle/src/atuin_client/record/encryption.rs
@@ -54,16 +54,16 @@ that happens in the background can make the network calls to the HSM
impl Encryption for PASETO_V4 {
fn re_encrypt(
mut data: EncryptedData,
- _ad: AdditionalData,
+ _ad: AdditionalData<'_>,
old_key: &[u8; 32],
new_key: &[u8; 32],
) -> Result<EncryptedData> {
- let cek = Self::decrypt_cek(data.content_encryption_key, old_key)?;
+ let cek = Self::decrypt_cek(&data.content_encryption_key, old_key)?;
data.content_encryption_key = Self::encrypt_cek(cek, new_key);
Ok(data)
}
- fn encrypt(data: DecryptedData, ad: AdditionalData, key: &[u8; 32]) -> EncryptedData {
+ fn encrypt(data: DecryptedData, ad: AdditionalData<'_>, key: &[u8; 32]) -> EncryptedData {
// generate a random key for this entry
// aka content-encryption-key (CEK)
let random_key = Key::<V4, Local>::new_os_random();
@@ -91,9 +91,13 @@ impl Encryption for PASETO_V4 {
}
}
- fn decrypt(data: EncryptedData, ad: AdditionalData, key: &[u8; 32]) -> Result<DecryptedData> {
+ fn decrypt(
+ data: EncryptedData,
+ ad: AdditionalData<'_>,
+ key: &[u8; 32],
+ ) -> Result<DecryptedData> {
let token = data.data;
- let cek = Self::decrypt_cek(data.content_encryption_key, key)?;
+ let cek = Self::decrypt_cek(&data.content_encryption_key, key)?;
// encode the implicit assertions
let assertions = Assertions::from(ad).encode();
@@ -114,12 +118,12 @@ impl Encryption for PASETO_V4 {
}
impl PASETO_V4 {
- fn decrypt_cek(wrapped_cek: String, key: &[u8; 32]) -> Result<Key<V4, Local>> {
+ fn decrypt_cek(wrapped_cek: &str, key: &[u8; 32]) -> Result<Key<V4, Local>> {
let wrapping_key = Key::<V4, Local>::from_bytes(*key);
// let wrapping_key = PasetoSymmetricKey::from(Key::from(key));
- let AtuinFooter { kid, wpk } = serde_json::from_str(&wrapped_cek)
+ let AtuinFooter { kid, wpk } = serde_json::from_str(wrapped_cek)
.context("wrapped cek did not contain the correct contents")?;
// check that the wrapping key matches the required key to decrypt.
@@ -262,7 +266,7 @@ mod tests {
let data = DecryptedData(vec![1, 2, 3, 4]);
let encrypted = PASETO_V4::encrypt(data, ad, &key.to_bytes());
- let _ = PASETO_V4::decrypt(encrypted, ad, &fake_key.to_bytes()).unwrap_err();
+ drop(PASETO_V4::decrypt(encrypted, ad, &fake_key.to_bytes()).unwrap_err());
}
#[test]
@@ -285,7 +289,7 @@ mod tests {
id: &RecordId(uuid_v7()),
..ad
};
- let _ = PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap_err();
+ drop(PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap_err());
}
#[test]
diff --git a/crates/turtle/src/atuin_client/record/sqlite_store.rs b/crates/turtle/src/atuin_client/record/sqlite_store.rs
index 24188443..12f9fc4e 100644
--- a/crates/turtle/src/atuin_client/record/sqlite_store.rs
+++ b/crates/turtle/src/atuin_client/record/sqlite_store.rs
@@ -353,18 +353,17 @@ impl SqliteStore {
pub(crate) async fn purge(&self, key: &[u8; 32]) -> Result<()> {
let all = self.load_all().await?;
- for record in all.iter() {
- match record.clone().decrypt::<PASETO_V4>(key) {
- Ok(_) => continue,
- Err(_) => {
- println!(
- "Failed to decrypt {}, deleting",
- record.id.0.as_hyphenated()
- );
-
- self.delete(record.id).await?;
- }
+ for record in &all {
+ if record.clone().decrypt::<PASETO_V4>(key).is_ok() {
+ continue;
}
+
+ println!(
+ "Failed to decrypt {}, deleting",
+ record.id.0.as_hyphenated()
+ );
+
+ self.delete(record.id).await?;
}
Ok(())
@@ -379,7 +378,6 @@ mod tests {
settings::test_local_timeout,
},
atuin_common::{
- self,
record::{DecryptedData, EncryptedData, Host, HostId, Record},
utils::uuid_v7,
},
@@ -389,9 +387,9 @@ mod tests {
fn test_record() -> Record<EncryptedData> {
Record::builder()
- .host(Host::new(HostId(atuin_common::utils::uuid_v7())))
+ .host(Host::new(HostId(uuid_v7())))
.version("v1".into())
- .tag(atuin_common::utils::uuid_v7().simple().to_string())
+ .tag(uuid_v7().simple().to_string())
.data(EncryptedData {
data: "1234".into(),
content_encryption_key: "1234".into(),
@@ -507,80 +505,6 @@ mod tests {
}
#[tokio::test]
- async fn len_different_tags() {
- let db = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
-
- // these have different tags, so the len should be the same
- // we model multiple stores within one database
- // new store = new tag = independent length
- let first = test_record();
- let second = test_record();
-
- db.push(&first).await.unwrap();
- db.push(&second).await.unwrap();
-
- let first_len = db.len(first.host.id, first.tag.as_str()).await.unwrap();
- let second_len = db.len(second.host.id, second.tag.as_str()).await.unwrap();
-
- assert_eq!(first_len, 1, "expected length of 1 after insert");
- assert_eq!(second_len, 1, "expected length of 1 after insert");
- }
-
- #[tokio::test]
- async fn append_a_bunch() {
- let db = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
-
- let mut tail = test_record();
- db.push(&tail).await.expect("failed to push record");
-
- for _ in 1..100 {
- tail = tail.append(vec![1, 2, 3, 4]).encrypt::<PASETO_V4>(&[0; 32]);
- db.push(&tail).await.unwrap();
- }
-
- assert_eq!(
- db.len(tail.host.id, tail.tag.as_str()).await.unwrap(),
- 100,
- "failed to insert 100 records"
- );
-
- assert_eq!(
- db.len_tag(tail.tag.as_str()).await.unwrap(),
- 100,
- "failed to insert 100 records"
- );
- }
-
- #[tokio::test]
- async fn append_a_big_bunch() {
- let db = SqliteStore::new(":memory:", test_local_timeout())
- .await
- .unwrap();
-
- let mut records: Vec<Record<EncryptedData>> = Vec::with_capacity(10000);
-
- let mut tail = test_record();
- records.push(tail.clone());
-
- for _ in 1..10000 {
- tail = tail.append(vec![1, 2, 3]).encrypt::<PASETO_V4>(&[0; 32]);
- records.push(tail.clone());
- }
-
- db.push_batch(records.iter()).await.unwrap();
-
- assert_eq!(
- db.len(tail.host.id, tail.tag.as_str()).await.unwrap(),
- 10000,
- "failed to insert 10k records"
- );
- }
-
- #[tokio::test]
async fn re_encrypt() {
let store = SqliteStore::new(":memory:", test_local_timeout())
.await
diff --git a/crates/turtle/src/atuin_client/record/sync.rs b/crates/turtle/src/atuin_client/record/sync.rs
index a86fc7a9..3057bb10 100644
--- a/crates/turtle/src/atuin_client/record/sync.rs
+++ b/crates/turtle/src/atuin_client/record/sync.rs
@@ -14,9 +14,6 @@ use indicatif::{ProgressBar, ProgressState, ProgressStyle};
#[derive(Error, Debug)]
pub(crate) enum SyncError {
- #[error("the local store is ahead of the remote, but for another host. has remote lost data?")]
- LocalAheadOtherHost,
-
#[error("an issue with the local database occurred: {msg:?}")]
LocalStoreError { msg: String },
@@ -319,7 +316,7 @@ pub(crate) async fn sync_remote(
downloaded.append(&mut d);
}
- Operation::Noop { .. } => continue,
+ Operation::Noop { .. } => (),
}
}
@@ -376,14 +373,13 @@ pub(crate) async fn sync(
#[cfg(test)]
mod tests {
+ use crate::atuin_client::record::sync::Operation;
use crate::atuin_common::record::{Diff, EncryptedData, HostId, Record};
- use pretty_assertions::assert_eq;
use crate::atuin_client::{
record::{
- encryption::PASETO_V4,
sqlite_store::SqliteStore,
- sync::{self, Operation},
+ sync::{self},
},
settings::test_local_timeout,
};
@@ -442,7 +438,7 @@ mod tests {
assert_eq!(diff.len(), 1);
- let operations = sync::operations(diff, &store).await.unwrap();
+ let operations = sync::operations(diff, &store).unwrap();
assert_eq!(operations.len(), 1);
@@ -456,211 +452,4 @@ mod tests {
}
);
}
-
- #[tokio::test]
- async fn build_two_way_diff() {
- // a diff where local is ahead of remote for one, and remote for
- // another. One upload, one download
-
- let shared_record = test_record();
- let remote_ahead = test_record();
-
- let local_ahead = shared_record
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- assert_eq!(local_ahead.idx, 1);
-
- let local = vec![shared_record.clone(), local_ahead.clone()]; // local knows about the already synced, and something newer in the same store
- let remote = vec![shared_record.clone(), remote_ahead.clone()]; // remote knows about the already-synced, and one new record in a new store
-
- let (store, diff) = build_test_diff(local, remote).await;
- let operations = sync::operations(diff, &store).await.unwrap();
-
- assert_eq!(operations.len(), 2);
-
- assert_eq!(
- operations,
- vec![
- // Or in otherwords, local is ahead by one
- Operation::Upload {
- host: local_ahead.host.id,
- tag: local_ahead.tag,
- local: 1,
- remote: Some(0),
- },
- // Or in other words, remote knows of a record in an entirely new store (tag)
- Operation::Download {
- host: remote_ahead.host.id,
- tag: remote_ahead.tag,
- local: None,
- remote: 0,
- },
- ]
- );
- }
-
- #[tokio::test]
- async fn build_complex_diff() {
- // One shared, ahead but known only by remote
- // One known only by local
- // One known only by remote
-
- let shared_record = test_record();
- let local_only = test_record();
-
- let local_only_20 = test_record();
- let local_only_21 = local_only_20
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let local_only_22 = local_only_21
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let local_only_23 = local_only_22
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let remote_only = test_record();
-
- let remote_only_20 = test_record();
- let remote_only_21 = remote_only_20
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let remote_only_22 = remote_only_21
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let remote_only_23 = remote_only_22
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let remote_only_24 = remote_only_23
- .append(vec![2, 3, 2])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let second_shared = test_record();
- let second_shared_remote_ahead = second_shared
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let second_shared_remote_ahead2 = second_shared_remote_ahead
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let third_shared = test_record();
- let third_shared_local_ahead = third_shared
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let third_shared_local_ahead2 = third_shared_local_ahead
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let fourth_shared = test_record();
- let fourth_shared_remote_ahead = fourth_shared
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
- let fourth_shared_remote_ahead2 = fourth_shared_remote_ahead
- .append(vec![1, 2, 3])
- .encrypt::<PASETO_V4>(&[0; 32]);
-
- let local = vec![
- shared_record.clone(),
- second_shared.clone(),
- third_shared.clone(),
- fourth_shared.clone(),
- fourth_shared_remote_ahead.clone(),
- // single store, only local has it
- local_only.clone(),
- // bigger store, also only known by local
- local_only_20.clone(),
- local_only_21.clone(),
- local_only_22.clone(),
- local_only_23.clone(),
- // another shared store, but local is ahead on this one
- third_shared_local_ahead.clone(),
- third_shared_local_ahead2.clone(),
- ];
-
- let remote = vec![
- remote_only.clone(),
- remote_only_20.clone(),
- remote_only_21.clone(),
- remote_only_22.clone(),
- remote_only_23.clone(),
- remote_only_24.clone(),
- shared_record.clone(),
- second_shared.clone(),
- third_shared.clone(),
- second_shared_remote_ahead.clone(),
- second_shared_remote_ahead2.clone(),
- fourth_shared.clone(),
- fourth_shared_remote_ahead.clone(),
- fourth_shared_remote_ahead2.clone(),
- ]; // remote knows about the already-synced, and one new record in a new store
-
- let (store, diff) = build_test_diff(local, remote).await;
- let operations = sync::operations(diff, &store).await.unwrap();
-
- assert_eq!(operations.len(), 7);
-
- let mut result_ops = vec![
- // We started with a shared record, but the remote knows of two newer records in the
- // same store
- Operation::Download {
- local: Some(0),
- remote: 2,
- host: second_shared_remote_ahead.host.id,
- tag: second_shared_remote_ahead.tag,
- },
- // We have a shared record, local knows of the first two but not the last
- Operation::Download {
- local: Some(1),
- remote: 2,
- host: fourth_shared_remote_ahead2.host.id,
- tag: fourth_shared_remote_ahead2.tag,
- },
- // Remote knows of a store with a single record that local does not have
- Operation::Download {
- local: None,
- remote: 0,
- host: remote_only.host.id,
- tag: remote_only.tag,
- },
- // Remote knows of a store with a bunch of records that local does not have
- Operation::Download {
- local: None,
- remote: 4,
- host: remote_only_20.host.id,
- tag: remote_only_20.tag,
- },
- // Local knows of a record in a store that remote does not have
- Operation::Upload {
- local: 0,
- remote: None,
- host: local_only.host.id,
- tag: local_only.tag,
- },
- // Local knows of 4 records in a store that remote does not have
- Operation::Upload {
- local: 3,
- remote: None,
- host: local_only_20.host.id,
- tag: local_only_20.tag,
- },
- // Local knows of 2 more records in a shared store that remote only has one of
- Operation::Upload {
- local: 2,
- remote: Some(0),
- host: third_shared.host.id,
- tag: third_shared.tag,
- },
- ];
-
- result_ops.sort_by_key(|op| match op {
- Operation::Noop { host, tag } => (0, *host, tag.clone()),
-
- Operation::Upload { host, tag, .. } => (1, *host, tag.clone()),
-
- Operation::Download { host, tag, .. } => (2, *host, tag.clone()),
- });
-
- assert_eq!(result_ops, operations);
- }
}
diff --git a/crates/turtle/src/atuin_client/secrets.rs b/crates/turtle/src/atuin_client/secrets.rs
index 30723890..74d47ea6 100644
--- a/crates/turtle/src/atuin_client/secrets.rs
+++ b/crates/turtle/src/atuin_client/secrets.rs
@@ -16,7 +16,7 @@ type SpType<'a> = &'a [(&'a str, &'a str, TestValue<'a>)];
type SpType<'a> = &'a [(&'a str, &'a str)];
/// A list of `(name, regex, test)`, where `test` should match against `regex`.
-pub(crate) static SECRET_PATTERNS: SpType = &[
+pub(crate) static SECRET_PATTERNS: SpType<'_> = &[
(
"AWS Access Key ID",
"A[KS]IA[0-9A-Z]{16}",
diff --git a/crates/turtle/src/atuin_client/settings.rs b/crates/turtle/src/atuin_client/settings.rs
index c966ba67..074b2634 100644
--- a/crates/turtle/src/atuin_client/settings.rs
+++ b/crates/turtle/src/atuin_client/settings.rs
@@ -47,7 +47,7 @@ pub(crate) enum SearchMode {
}
impl SearchMode {
- pub(crate) fn as_str(&self) -> &'static str {
+ pub(crate) fn as_str(self) -> &'static str {
match self {
SearchMode::Prefix => "PREFIX",
SearchMode::FullText => "FULLTXT",
@@ -56,7 +56,7 @@ impl SearchMode {
SearchMode::DaemonFuzzy => "DAEMON",
}
}
- pub(crate) fn next(&self, settings: &Settings) -> Self {
+ pub(crate) fn next(self, settings: &Settings) -> Self {
match self {
SearchMode::Prefix => SearchMode::FullText,
// if the user is using skim, we go to skim
@@ -94,7 +94,7 @@ pub(crate) enum FilterMode {
}
impl FilterMode {
- pub(crate) fn as_str(&self) -> &'static str {
+ pub(crate) fn as_str(self) -> &'static str {
match self {
FilterMode::Global => "GLOBAL",
FilterMode::Host => "HOST",
@@ -427,7 +427,7 @@ pub(crate) struct Search {
pub(crate) frecency_score_multiplier: f64,
}
-/// Log level for file logging. Maps to tracing's LevelFilter.
+/// Log level for file logging. Maps to tracing's [`LevelFilter`].
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum LogLevel {
@@ -440,8 +440,8 @@ pub(crate) enum LogLevel {
}
impl LogLevel {
- /// Convert to a tracing directive string for use with EnvFilter.
- pub(crate) fn as_directive(&self) -> &'static str {
+ /// Convert to a tracing directive string for use with [`EnvFilter`].
+ pub(crate) fn as_directive(self) -> &'static str {
match self {
LogLevel::Trace => "trace",
LogLevel::Debug => "debug",
@@ -478,7 +478,7 @@ pub(crate) struct Logs {
pub(crate) dir: String,
/// Default log level for file logging. Defaults to "info".
- /// Note: ATUIN_LOG environment variable overrides this.
+ /// Note: [`ATUIN_LOG`] environment variable overrides this.
#[serde(default)]
pub(crate) level: LogLevel,
@@ -641,7 +641,7 @@ pub(crate) enum UiColumnType {
impl UiColumnType {
/// Returns the default width for this column type (in characters).
/// The Command column returns 0 as it expands to fill remaining space.
- pub(crate) fn default_width(&self) -> u16 {
+ pub(crate) fn default_width(self) -> u16 {
match self {
UiColumnType::Duration => 5, // "814ms"
UiColumnType::Time => 9, // "459ms ago"
@@ -683,7 +683,7 @@ impl UiColumn {
// Custom deserialize to handle both string and object formats:
// "duration" or { type = "duration", width = 8, expand = true }
-impl<'de> serde::Deserialize<'de> for UiColumn {
+impl<'de> Deserialize<'de> for UiColumn {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
@@ -695,7 +695,7 @@ impl<'de> serde::Deserialize<'de> for UiColumn {
impl<'de> Visitor<'de> for UiColumnVisitor {
type Value = UiColumn;
- fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"a column type string or an object with 'type' and optional 'width'/'expand'",
)
@@ -706,7 +706,7 @@ impl<'de> serde::Deserialize<'de> for UiColumn {
E: de::Error,
{
let column_type: UiColumnType =
- serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(value))?;
+ Deserialize::deserialize(de::value::StrDeserializer::new(value))?;
Ok(UiColumn::new(column_type))
}
@@ -730,7 +730,7 @@ impl<'de> serde::Deserialize<'de> for UiColumn {
expand = Some(map.next_value()?);
}
_ => {
- let _: serde::de::IgnoredAny = map.next_value()?;
+ let _: de::IgnoredAny = map.next_value()?;
}
}
}
@@ -979,18 +979,19 @@ impl Settings {
}
pub(crate) fn builder() -> Result<ConfigBuilder<DefaultState>> {
- Self::builder_with_data_dir(&crate::atuin_common::utils::data_dir())
+ Self::builder_with_data_dir(&utils::data_dir())
}
+ #[expect(clippy::too_many_lines)]
fn builder_with_data_dir(data_dir: &std::path::Path) -> Result<ConfigBuilder<DefaultState>> {
let db_path = data_dir.join("history.db");
let record_store_path = data_dir.join("records.db");
let kv_path = data_dir.join("kv.db");
let scripts_path = data_dir.join("scripts.db");
let ai_sessions_path = data_dir.join("ai_sessions.db");
- let socket_path = crate::atuin_common::utils::runtime_dir().join("atuin.sock");
+ let socket_path = utils::runtime_dir().join("atuin.sock");
let pidfile_path = data_dir.join("atuin-daemon.pid");
- let logs_dir = crate::atuin_common::utils::logs_dir();
+ let logs_dir = utils::logs_dir();
let key_path = data_dir.join("key");
let meta_path = data_dir.join("meta.db");
@@ -1091,10 +1092,10 @@ impl Settings {
.set_default("tmux.height", "60%")?
.set_default(
"prefers_reduced_motion",
- std::env::var("NO_MOTION")
- .ok()
- .map(|_| config::Value::new(None, config::ValueKind::Boolean(true)))
- .unwrap_or_else(|| config::Value::new(None, config::ValueKind::Boolean(false))),
+ std::env::var("NO_MOTION").ok().map_or_else(
+ || config::Value::new(None, config::ValueKind::Boolean(false)),
+ |_| config::Value::new(None, config::ValueKind::Boolean(true)),
+ ),
)?
.set_default("no_mouse", false)?
.add_source(
@@ -1105,18 +1106,19 @@ impl Settings {
}
pub(crate) fn get_config_path() -> Result<PathBuf> {
- let config_dir = crate::atuin_common::utils::config_dir();
+ let config_dir = utils::config_dir();
create_dir_all(&config_dir)
.wrap_err_with(|| format!("could not create dir {}", config_dir.display()))?;
- let mut config_file = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") {
- PathBuf::from(p)
- } else {
- let mut config_file = PathBuf::new();
- config_file.push(config_dir);
- config_file
- };
+ let mut config_file = std::env::var("ATUIN_CONFIG_DIR").map_or_else(
+ |_| {
+ let mut config_file = PathBuf::new();
+ config_file.push(config_dir);
+ config_file
+ },
+ PathBuf::from,
+ );
config_file.push("config.toml");
@@ -1162,16 +1164,16 @@ impl Settings {
.map_err(|e| eyre!("failed to expand data_dir path: {}", e))?;
PathBuf::from(expanded.as_ref())
}
- None => crate::atuin_common::utils::data_dir(),
+ None => utils::data_dir(),
}
} else {
- crate::atuin_common::utils::data_dir()
+ utils::data_dir()
};
DATA_DIR.set(effective_data_dir.clone()).ok();
create_dir_all(&effective_data_dir)
- .wrap_err_with(|| format!("could not create dir {effective_data_dir:?}"))?;
+ .wrap_err_with(|| format!("could not create dir {}", effective_data_dir.display()))?;
let mut config_builder = Self::builder_with_data_dir(&effective_data_dir)?;
@@ -1205,7 +1207,7 @@ impl Settings {
]
.iter()
.map(|key| (key, built.get_string(key).unwrap_or_default()))
- .filter_map(|(key, value)| match Self::expand_path(value) {
+ .filter_map(|(key, value)| match Self::expand_path(&value) {
Ok(expanded) => Some((key, expanded)),
Err(e) => {
log::warn!("failed to expand path for {key}: {e}");
@@ -1263,7 +1265,7 @@ impl Settings {
let full_key = if prefix.is_empty() {
k.clone()
} else {
- format!("{}.{}", prefix, k)
+ format!("{prefix}.{k}")
};
match &v.kind {
@@ -1302,7 +1304,7 @@ impl Settings {
Ok(settings)
}
- fn expand_path(path: String) -> Result<String> {
+ fn expand_path(path: &str) -> Result<String> {
shellexpand::full(&path)
.map(|p| p.to_string())
.map_err(|e| eyre!("failed to expand path: {}", e))
diff --git a/crates/turtle/src/atuin_client/settings/watcher.rs b/crates/turtle/src/atuin_client/settings/watcher.rs
index e280480c..5eec3692 100644
--- a/crates/turtle/src/atuin_client/settings/watcher.rs
+++ b/crates/turtle/src/atuin_client/settings/watcher.rs
@@ -134,7 +134,7 @@ impl SettingsWatcher {
warn!(
"config watcher: event has no paths, triggering reload to be safe"
);
- let _ = debounce_tx.send(());
+ debounce_tx.send(()).expect("should still be active");
return;
}
@@ -163,7 +163,7 @@ impl SettingsWatcher {
) {
debug!("config file event detected: {event:?}");
// Send to debounce channel (ignore send errors - receiver might be gone)
- let _ = debounce_tx.send(());
+ debounce_tx.send(()).ok();
}
}
Err(e) => {