Skip to content

Commit 7460ac3

Browse files
committed
feat: delete duplicate history
1 parent a45b4c5 commit 7460ac3

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

Diff for: crates/atuin-client/src/database.rs

+22
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ pub trait Database: Send + Sync + 'static {
119119
async fn all_with_count(&self) -> Result<Vec<(History, i32)>>;
120120

121121
async fn stats(&self, h: &History) -> Result<HistoryStats>;
122+
123+
async fn get_dups(&self, before: i64, dupkeep: u32) -> Result<Vec<History>>;
122124
}
123125

124126
// Intended for use on a developer machine and not a sync server.
@@ -768,6 +770,26 @@ impl Database for Sqlite {
768770
duration_over_time,
769771
})
770772
}
773+
774+
async fn get_dups(&self, before: i64, dupkeep: u32) -> Result<Vec<History>> {
775+
let res = sqlx::query(
776+
"SELECT * FROM (
777+
SELECT *, ROW_NUMBER()
778+
OVER (PARTITION BY command, cwd, hostname ORDER BY timestamp DESC)
779+
AS rn
780+
FROM history
781+
) sub
782+
WHERE rn > ?1 and timestamp < ?2;
783+
",
784+
)
785+
.bind(dupkeep)
786+
.bind(before)
787+
.map(Self::query_history)
788+
.fetch_all(&self.pool)
789+
.await?;
790+
791+
Ok(res)
792+
}
771793
}
772794

773795
trait SqlBuilderExt {

Diff for: crates/atuin/src/command/client/history.rs

+77
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@ pub enum Cmd {
117117
#[arg(short = 'n', long)]
118118
dry_run: bool,
119119
},
120+
121+
/// Delete duplicate history entries (that have the same command, cwd and hostname)
122+
Dedup {
123+
/// List matching history lines without performing the actual deletion.
124+
#[arg(short = 'n', long)]
125+
dry_run: bool,
126+
127+
/// Only delete results added before this date
128+
#[arg(long, short)]
129+
before: String,
130+
131+
/// How many recent duplicates to keep
132+
#[arg(long)]
133+
dupkeep: u32,
134+
},
120135
}
121136

122137
#[derive(Clone, Copy, Debug)]
@@ -544,6 +559,54 @@ impl Cmd {
544559
Ok(())
545560
}
546561

562+
async fn handle_dedup(
563+
db: &impl Database,
564+
settings: &Settings,
565+
store: SqliteStore,
566+
before: i64,
567+
dupkeep: u32,
568+
dry_run: bool,
569+
) -> Result<()> {
570+
let matches: Vec<History> = db.get_dups(before, dupkeep).await?;
571+
572+
match matches.len() {
573+
0 => {
574+
println!("No duplicates to delete.");
575+
return Ok(());
576+
}
577+
1 => println!("Found 1 duplicate to delete."),
578+
n => println!("Found {n} duplicates to delete."),
579+
}
580+
581+
if dry_run {
582+
print_list(
583+
&matches,
584+
ListMode::Human,
585+
Some(settings.history_format.as_str()),
586+
false,
587+
false,
588+
settings.timezone,
589+
);
590+
} else {
591+
let encryption_key: [u8; 32] = encryption::load_key(settings)
592+
.context("could not load encryption key")?
593+
.into();
594+
let host_id = Settings::host_id().expect("failed to get host_id");
595+
let history_store = HistoryStore::new(store.clone(), host_id, encryption_key);
596+
597+
for entry in matches {
598+
eprintln!("deleting {}", entry.id);
599+
if settings.sync.records {
600+
let (id, _) = history_store.delete(entry.id).await?;
601+
history_store.incremental_build(db, &[id]).await?;
602+
} else {
603+
db.delete(entry).await?;
604+
}
605+
}
606+
}
607+
Ok(())
608+
}
609+
547610
pub async fn run(self, settings: &Settings) -> Result<()> {
548611
let context = current_context();
549612

@@ -628,6 +691,20 @@ impl Cmd {
628691
Self::Prune { dry_run } => {
629692
Self::handle_prune(&db, settings, store, context, dry_run).await
630693
}
694+
695+
Self::Dedup {
696+
dry_run,
697+
before,
698+
dupkeep,
699+
} => {
700+
let before = i64::try_from(
701+
interim::parse_date_string(
702+
before.as_str(),
703+
OffsetDateTime::now_utc(),
704+
interim::Dialect::Uk,
705+
)?.unix_timestamp_nanos())?;
706+
Self::handle_dedup(&db, settings, store, before, dupkeep, dry_run).await
707+
}
631708
}
632709
}
633710
}

0 commit comments

Comments
 (0)