Add config option to filter out duplicate history entries as requested in #10

This commit is contained in:
David Dworken 2022-11-03 20:36:36 -07:00
parent 83ca625ad9
commit 6dea8a989e
No known key found for this signature in database
12 changed files with 162 additions and 8 deletions

View File

@ -71,7 +71,6 @@ hishtory config-set displayed-columns CWD Command
</details>
<details>
<summary>Custom Columns</summary>
@ -81,7 +80,6 @@ You can create custom column definitions that are populated from arbitrary comma
hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true'
hishtory config-add displayed-columns git_remote
```
</details>
<details>
@ -89,6 +87,15 @@ hishtory config-add displayed-columns git_remote
If you'd like to disable the control-R integration in your shell, you can do so by running `hishtory config-set enable-control-r false`.
</details>
<details>
<summary>Filtering duplicate entries</summary>
By default, hishtory query will show all results even if this includes duplicate history entries. This helps you keep track of how many times you've run a command and in what contexts. If you'd rather disable this so that hiSHtory won't show duplicate entries, you can run:
```
hishtory config-set filter-duplicate-commands true
```
</details>
<details>
<summary>Offline Install</summary>
If you don't need the ability to sync your shell history, you can install hiSHtory in offline mode.

View File

@ -2069,6 +2069,38 @@ echo bar`)
}
}
func TestRemoveDuplicateRows(t *testing.T) {
// Setup
tester := bashTester{}
defer testutils.BackupAndRestore(t)()
installHishtory(t, tester, "")
// Record a few commands and check that they get recorded and all are displayed in a table
tester.RunInteractiveShell(t, `echo foo
echo foo
echo baz
echo baz
echo foo`)
out := tester.RunInteractiveShell(t, `hishtory export -pipefail`)
compareGoldens(t, out, "testRemoveDuplicateRows-export")
tester.RunInteractiveShell(t, `hishtory config-set displayed-columns 'Exit Code' Command`)
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
compareGoldens(t, out, "testRemoveDuplicateRows-query")
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"})
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
compareGoldens(t, out, "testRemoveDuplicateRows-tquery")
// And change the config to filter out duplicate rows
tester.RunInteractiveShell(t, `hishtory config-set filter-duplicate-commands true`)
out = tester.RunInteractiveShell(t, `hishtory export -pipefail`)
compareGoldens(t, out, "testRemoveDuplicateRows-enabled-export")
out = tester.RunInteractiveShell(t, `hishtory query -pipefail`)
compareGoldens(t, out, "testRemoveDuplicateRows-enabled-query")
out = captureTerminalOutput(t, tester, []string{"hishtory SPACE tquery SPACE -pipefail ENTER"})
out = strings.TrimSpace(strings.Split(out, "hishtory tquery")[1])
compareGoldens(t, out, "testRemoveDuplicateRows-enabled-tquery")
}
type deviceSet struct {
deviceMap *map[device]deviceOp
currentDevice *device

View File

@ -163,6 +163,8 @@ type ClientConfig struct {
CustomColumns []CustomColumnDefinition `json:"custom_columns"`
// Whether this is an offline instance of hishtory with no syncing
IsOffline bool `json:"is_offline"`
// Whether duplicate commands should be displayed
FilterDuplicateCommands bool `json:"filter_duplicate_commands"`
}
type CustomColumnDefinition struct {

View File

@ -0,0 +1,8 @@
echo foo
echo foo
echo baz
echo baz
echo foo
hishtory config-set displayed-columns 'Exit Code' Command
source /Users/david/.bashrc
hishtory config-set filter-duplicate-commands true

View File

@ -0,0 +1,7 @@
Exit Code Command
0 hishtory config-set filter-duplicate-commands true
0 source /Users/david/.bashrc
0 hishtory config-set displayed-columns 'Exit Code' Command
0 echo foo
0 echo baz
0 echo foo

View File

@ -0,0 +1,30 @@
-pipefail
Search Query: > -pipefail
┌───────────────────────────────────────────────────────────────────────────┐
│ Exit Code Command │
│───────────────────────────────────────────────────────────────────────────│
│ 0 source /Users/david/.bashrc │
│ 0 hishtory config-set filter-duplicate-commands true │
│ 0 source /Users/david/.bashrc │
│ 0 hishtory config-set displayed-columns 'Exit Code' Command │
│ 0 echo foo │
│ 0 echo baz │
│ 0 echo foo │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────┘

View File

@ -0,0 +1,5 @@
echo foo
echo foo
echo baz
echo baz
echo foo

View File

@ -0,0 +1,7 @@
Exit Code Command
0 hishtory config-set displayed-columns 'Exit Code' Command
0 echo foo
0 echo baz
0 echo baz
0 echo foo
0 echo foo

View File

@ -0,0 +1,30 @@
-pipefail
Search Query: > -pipefail
┌───────────────────────────────────────────────────────────────────────────┐
│ Exit Code Command │
│───────────────────────────────────────────────────────────────────────────│
│ 0 source /Users/david/.bashrc │
│ 0 hishtory config-set displayed-columns 'Exit Code' Command │
│ 0 echo foo │
│ 0 echo baz │
│ 0 echo baz │
│ 0 echo foo │
│ 0 echo foo │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────┘

View File

@ -461,7 +461,7 @@ func stringArrayToAnyArray(arr []string) []any {
return ret
}
func DisplayResults(ctx *context.Context, results []*data.HistoryEntry) error {
func DisplayResults(ctx *context.Context, results []*data.HistoryEntry, numResults int) error {
config := hctx.GetConf(ctx)
headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc()
@ -472,12 +472,22 @@ func DisplayResults(ctx *context.Context, results []*data.HistoryEntry) error {
tbl := table.New(columns...)
tbl.WithHeaderFormatter(headerFmt)
for _, result := range results {
row, err := buildTableRow(ctx, config.DisplayedColumns, *result)
lastCommand := ""
numRows := 0
for _, entry := range results {
if entry != nil && entry.Command == lastCommand && config.FilterDuplicateCommands {
continue
}
row, err := buildTableRow(ctx, config.DisplayedColumns, *entry)
if err != nil {
return err
}
tbl.AddRow(stringArrayToAnyArray(row)...)
numRows += 1
lastCommand = entry.Command
if numRows >= numResults {
break
}
}
tbl.Print()

View File

@ -20,7 +20,7 @@ import (
)
const TABLE_HEIGHT = 20
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 3
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
var selectedRow string = ""
@ -212,20 +212,26 @@ func (m model) View() string {
func getRows(ctx *context.Context, columnNames []string, query string, numEntries int) ([]table.Row, int, error) {
db := hctx.GetDb(ctx)
config := hctx.GetConf(ctx)
data, err := Search(ctx, db, query, numEntries)
if err != nil {
return nil, 0, err
}
var rows []table.Row
lastCommand := ""
for i := 0; i < numEntries; i++ {
if i < len(data) {
entry := data[i]
if entry.Command == lastCommand && config.FilterDuplicateCommands {
continue
}
entry.Command = strings.ReplaceAll(entry.Command, "\n", " ") // TODO: handle multi-line commands better here
row, err := buildTableRow(ctx, columnNames, *entry)
if err != nil {
return nil, 0, fmt.Errorf("failed to build row for entry=%#v: %v", entry, err)
}
rows = append(rows, row)
lastCommand = entry.Command
} else {
rows = append(rows, table.Row{})
}

View File

@ -135,6 +135,8 @@ func main() {
switch key {
case "enable-control-r":
fmt.Printf("%v", config.ControlRSearchEnabled)
case "filter-duplicate-commands":
fmt.Printf("%v", config.FilterDuplicateCommands)
case "displayed-columns":
for _, col := range config.DisplayedColumns {
if strings.Contains(col, " ") {
@ -162,6 +164,13 @@ func main() {
}
config.ControlRSearchEnabled = (val == "true")
lib.CheckFatalError(hctx.SetConfig(config))
case "filter-duplicate-commands":
val := os.Args[3]
if val != "true" && val != "false" {
log.Fatalf("Unexpected config value %s, must be one of: true, false", val)
}
config.FilterDuplicateCommands = (val == "true")
lib.CheckFatalError(hctx.SetConfig(config))
case "displayed-columns":
vals := os.Args[3:]
config.DisplayedColumns = vals
@ -313,9 +322,10 @@ func query(ctx *context.Context, query string) {
}
}
lib.CheckFatalError(displayBannerIfSet(ctx))
data, err := lib.Search(ctx, db, query, 25)
numResults := 25
data, err := lib.Search(ctx, db, query, numResults*5)
lib.CheckFatalError(err)
lib.CheckFatalError(lib.DisplayResults(ctx, data))
lib.CheckFatalError(lib.DisplayResults(ctx, data, numResults))
}
func displayBannerIfSet(ctx *context.Context) error {