@@ -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,54 @@ 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
+ 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
+
547
610
pub async fn run ( self , settings : & Settings ) -> Result < ( ) > {
548
611
let context = current_context ( ) ;
549
612
@@ -628,6 +691,22 @@ impl Cmd {
628
691
Self :: Prune { dry_run } => {
629
692
Self :: handle_prune ( & db, settings, store, context, dry_run) . await
630
693
}
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
+ ) ?
706
+ . unix_timestamp_nanos ( ) ,
707
+ ) ?;
708
+ Self :: handle_dedup ( & db, settings, store, before, dupkeep, dry_run) . await
709
+ }
631
710
}
632
711
}
633
712
}
0 commit comments