@@ -117,6 +117,21 @@ pub enum Cmd {
117
117
#[ arg( short = 'n' , long) ]
118
118
dry_run : bool ,
119
119
} ,
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
+ }
120
135
}
121
136
122
137
#[ derive( Clone , Copy , Debug ) ]
@@ -544,6 +559,58 @@ impl Cmd {
544
559
Ok ( ( ) )
545
560
}
546
561
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
+ // Grab all executed commands and filter them using History::should_save.
571
+ // We could iterate or paginate here if memory usage becomes an issue.
572
+ let matches: Vec < History > = db
573
+ . get_dups ( before, dupkeep)
574
+ . await ?;
575
+
576
+ match matches. len ( ) {
577
+ 0 => {
578
+ println ! ( "No duplicates to delete." ) ;
579
+ return Ok ( ( ) ) ;
580
+ }
581
+ 1 => println ! ( "Found 1 duplicate to delete." ) ,
582
+ n => println ! ( "Found {n} duplicates to delete." ) ,
583
+ }
584
+
585
+ if dry_run {
586
+ print_list (
587
+ & matches,
588
+ ListMode :: Human ,
589
+ Some ( settings. history_format . as_str ( ) ) ,
590
+ false ,
591
+ false ,
592
+ settings. timezone ,
593
+ ) ;
594
+ } else {
595
+ let encryption_key: [ u8 ; 32 ] = encryption:: load_key ( settings)
596
+ . context ( "could not load encryption key" ) ?
597
+ . into ( ) ;
598
+ let host_id = Settings :: host_id ( ) . expect ( "failed to get host_id" ) ;
599
+ let history_store = HistoryStore :: new ( store. clone ( ) , host_id, encryption_key) ;
600
+
601
+ for entry in matches {
602
+ eprintln ! ( "deleting {}" , entry. id) ;
603
+ if settings. sync . records {
604
+ let ( id, _) = history_store. delete ( entry. id ) . await ?;
605
+ history_store. incremental_build ( db, & [ id] ) . await ?;
606
+ } else {
607
+ db. delete ( entry) . await ?;
608
+ }
609
+ }
610
+ }
611
+ Ok ( ( ) )
612
+ }
613
+
547
614
pub async fn run ( self , settings : & Settings ) -> Result < ( ) > {
548
615
let context = current_context ( ) ;
549
616
@@ -628,6 +695,15 @@ impl Cmd {
628
695
Self :: Prune { dry_run } => {
629
696
Self :: handle_prune ( & db, settings, store, context, dry_run) . await
630
697
}
698
+
699
+ Self :: Dedup { dry_run, before, dupkeep } => {
700
+ let before = interim:: parse_date_string (
701
+ before. as_str ( ) ,
702
+ OffsetDateTime :: now_utc ( ) ,
703
+ interim:: Dialect :: Uk ,
704
+ ) ?. unix_timestamp_nanos ( ) as i64 ;
705
+ Self :: handle_dedup ( & db, settings, store, before, dupkeep, dry_run) . await
706
+ }
631
707
}
632
708
}
633
709
}
0 commit comments