diff --git a/README.md b/README.md index a0a9b34..2b24f1f 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ hishtory config-set displayed-columns CWD Command You can create custom column definitions that are populated from arbitrary commands. For example, if you want to create a new column named `git_remote` that contains the git remote if the cwd is in a git directory, you can run: ``` -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 custom-columns 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 ``` diff --git a/backend/server/server.go b/backend/server/server.go index 2898410..b8f97e8 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -82,7 +82,7 @@ func usageStatsHandler(w http.ResponseWriter, r *http.Request) { COUNT(DISTINCT devices.device_id) as num_devices, SUM(usage_data.num_entries_handled) as num_history_entries, MAX(usage_data.last_used) as last_active, - COALESCE(STRING_AGG(DISTINCT usage_data.last_ip, ', ') FILTER (WHERE usage_data.last_ip != 'Unknown'), 'Unknown') as ip_addresses, + COALESCE(STRING_AGG(DISTINCT usage_data.last_ip, ', ') FILTER (WHERE usage_data.last_ip != 'Unknown' AND usage_data.last_ip != 'UnknownIp'), 'Unknown') as ip_addresses, COALESCE(SUM(usage_data.num_queries), 0) as num_queries, COALESCE(MAX(usage_data.last_queried), 'January 1, 1970') as last_queried, STRING_AGG(DISTINCT usage_data.version, ', ') as versions @@ -177,6 +177,7 @@ func apiBootstrapHandler(w http.ResponseWriter, r *http.Request) { tx := GLOBAL_DB.Where("user_id = ?", userId) var historyEntries []*shared.EncHistoryEntry checkGormResult(tx.Find(&historyEntries)) + fmt.Printf("apiBootstrapHandler: Found %d entries\n", len(historyEntries)) resp, err := json.Marshal(historyEntries) if err != nil { panic(err) @@ -216,7 +217,7 @@ func apiQueryHandler(w http.ResponseWriter, r *http.Request) { func getRemoteAddr(r *http.Request) string { addr, ok := r.Header["X-Real-Ip"] if !ok || len(addr) == 0 { - return "Unknown" + return "UnknownIp" } return addr[0] } diff --git a/client/client_test.go b/client/client_test.go index fcaadb8..111624a 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1206,6 +1206,10 @@ echo other`) } func testInstallViaPythonScript(t *testing.T, tester shellTester) { + if !testutils.IsOnline() { + t.Skip("skipping because we're currently offline") + } + // Set up defer testutils.BackupAndRestore(t)() defer testutils.BackupAndRestoreEnv("HISHTORY_TEST")() @@ -1314,7 +1318,7 @@ func testHelpCommand(t *testing.T, tester shellTester) { // Test the help command out := tester.RunInteractiveShell(t, `hishtory help`) - if !strings.HasPrefix(out, "hiSHtory: Better shell history\n\nSupported commands:\n") { + if !strings.HasPrefix(out, "hiSHtory: Better shell history") { t.Fatalf("expected hishtory help to contain intro, actual=%#v", out) } out2 := tester.RunInteractiveShell(t, `hishtory -h`) @@ -1531,27 +1535,27 @@ ls /tmp`, randomCmdUuid, randomCmdUuid) } // Redact foo - out = tester.RunInteractiveShell(t, `hishtory redact --force foo`) + out = tester.RunInteractiveShell(t, `HISHTORY_REDACT_FORCE=1 hishtory redact foo`) if out != "Permanently deleting 2 entries\n" { t.Fatalf("hishtory redact gave unexpected output=%#v", out) } // Check that the commands are redacted out = tester.RunInteractiveShell(t, `hishtory export | grep -v pipefail`) - expectedOutput = fmt.Sprintf("echo %s-bas\nls /tmp\nhishtory redact --force foo\n", randomCmdUuid) + expectedOutput = fmt.Sprintf("echo %s-bas\nls /tmp\nHISHTORY_REDACT_FORCE=1 hishtory redact foo\n", randomCmdUuid) if diff := cmp.Diff(expectedOutput, out); diff != "" { t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) } // Redact s - out = tester.RunInteractiveShell(t, `hishtory redact --force s`) + out = tester.RunInteractiveShell(t, `HISHTORY_REDACT_FORCE=1 hishtory redact s`) if out != "Permanently deleting 10 entries\n" && out != "Permanently deleting 11 entries\n" { t.Fatalf("hishtory redact gave unexpected output=%#v", out) } // Check that the commands are redacted out = tester.RunInteractiveShell(t, `hishtory export | grep -v pipefail`) - expectedOutput = "hishtory redact --force s\n" + expectedOutput = "HISHTORY_REDACT_FORCE=1 hishtory redact s\n" if diff := cmp.Diff(expectedOutput, out); diff != "" { t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) } @@ -1559,12 +1563,12 @@ ls /tmp`, randomCmdUuid, randomCmdUuid) // Record another command tester.RunInteractiveShell(t, `echo hello`) out = tester.RunInteractiveShell(t, `hishtory export | grep -v pipefail`) - expectedOutput = "hishtory redact --force s\necho hello\n" + expectedOutput = "HISHTORY_REDACT_FORCE=1 hishtory redact s\necho hello\n" if diff := cmp.Diff(expectedOutput, out); diff != "" { t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) } - // Redact it without --force + // Redact it without HISHTORY_REDACT_FORCE out, err := tester.RunInteractiveShellRelaxed(t, `yes | hishtory redact hello`) testutils.Check(t, err) if out != "This will permanently delete 1 entries, are you sure? [y/N]" { @@ -1573,7 +1577,7 @@ ls /tmp`, randomCmdUuid, randomCmdUuid) // And check it was redacted out = tester.RunInteractiveShell(t, `hishtory export | grep -v pipefail`) - expectedOutput = "hishtory redact --force s\nyes | hishtory redact hello\n" + expectedOutput = "HISHTORY_REDACT_FORCE=1 hishtory redact s\nyes | hishtory redact hello\n" if diff := cmp.Diff(expectedOutput, out); diff != "" { t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) } @@ -1614,14 +1618,14 @@ ls /tmp`, randomCmdUuid, randomCmdUuid) // Restore the first client, and redact some commands restoreInstall2 := testutils.BackupAndRestoreWithId(t, "-2") restoreInstall1() - out = tester.RunInteractiveShell(t, `hishtory redact --force `+randomCmdUuid) + out = tester.RunInteractiveShell(t, `HISHTORY_REDACT_FORCE=1 hishtory redact `+randomCmdUuid) if out != "Permanently deleting 2 entries\n" { t.Fatalf("hishtory redact gave unexpected output=%#v", out) } // Confirm that client1 doesn't have the commands out = tester.RunInteractiveShell(t, `hishtory export | grep -v pipefail`) - expectedOutput = fmt.Sprintf("echo foo\nls /tmp\nhishtory redact --force %s\n", randomCmdUuid) + expectedOutput = fmt.Sprintf("echo foo\nls /tmp\nHISHTORY_REDACT_FORCE=1 hishtory redact %s\n", randomCmdUuid) if diff := cmp.Diff(expectedOutput, out); diff != "" { t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out) } @@ -1641,17 +1645,17 @@ func testConfigGetSet(t *testing.T, tester shellTester) { // Config-get and set for enable-control-r out := tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`) - if out != "true" { + if out != "true\n" { t.Fatalf("unexpected config-get output: %#v", out) } tester.RunInteractiveShell(t, `hishtory config-set enable-control-r false`) out = tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`) - if out != "false" { + if out != "false\n" { t.Fatalf("unexpected config-get output: %#v", out) } tester.RunInteractiveShell(t, `hishtory config-set enable-control-r true`) out = tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`) - if out != "true" { + if out != "true\n" { t.Fatalf("unexpected config-get output: %#v", out) } @@ -1708,7 +1712,7 @@ func testHandleUpgradedFeatures(t *testing.T, tester shellTester) { // And check that hishtory says it is false by default out := tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`) - if out != "false" { + if out != "false\n" { t.Fatalf("unexpected config-get output: %#v", out) } @@ -1718,7 +1722,7 @@ func testHandleUpgradedFeatures(t *testing.T, tester shellTester) { // Now it should be enabled out = tester.RunInteractiveShell(t, `hishtory config-get enable-control-r`) - if out != "true" { + if out != "true\n" { t.Fatalf("unexpected config-get output: %#v", out) } } @@ -2073,7 +2077,7 @@ echo baz`) compareGoldens(t, out, "testCustomColumns-initHistory") // Configure a custom column - tester.RunInteractiveShell(t, `hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true'`) + tester.RunInteractiveShell(t, `hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true'`) // Run a few commands, some of which will have a git_remote out = tester.RunInteractiveShell(t, `echo foo @@ -2413,7 +2417,7 @@ func fuzzTest(t *testing.T, tester shellTester, input string) { testutils.Check(t, err) } if op.redactQuery != "" { - _, err := tester.RunInteractiveShellRelaxed(t, `hishtory redact --force `+op.redactQuery) + _, err := tester.RunInteractiveShellRelaxed(t, `HISHTORY_REDACT_FORCE=1 hishtory redact `+op.redactQuery) testutils.Check(t, err) } @@ -2436,7 +2440,7 @@ func fuzzTest(t *testing.T, tester shellTester, input string) { filteredLines = append(filteredLines, line) } val = strings.Join(filteredLines, "\n") - val += `hishtory redact --force ` + op.redactQuery + "\n" + val += `HISHTORY_REDACT_FORCE=1 hishtory redact ` + op.redactQuery + "\n" } keyToCommands[op.device.key] = val diff --git a/client/cmd/configAdd.go b/client/cmd/configAdd.go new file mode 100644 index 0000000..45ff04b --- /dev/null +++ b/client/cmd/configAdd.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var configAddCmd = &cobra.Command{ + Use: "config-add", + Short: "Add a config option", + GroupID: GROUP_ID_CONFIG, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +var addCustomColumnsCmd = &cobra.Command{ + Use: "custom-columns", + Short: "Add a custom column", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + columnName := args[0] + command := args[1] + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + if config.CustomColumns == nil { + config.CustomColumns = make([]hctx.CustomColumnDefinition, 0) + } + config.CustomColumns = append(config.CustomColumns, hctx.CustomColumnDefinition{ColumnName: columnName, ColumnCommand: command}) + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +var addDisplayedColumnsCmd = &cobra.Command{ + Use: "displayed-columns", + Short: "Add a column to be displayed", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + vals := args + config.DisplayedColumns = append(config.DisplayedColumns, vals...) + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +func init() { + rootCmd.AddCommand(configAddCmd) + configAddCmd.AddCommand(addCustomColumnsCmd) + configAddCmd.AddCommand(addDisplayedColumnsCmd) +} diff --git a/client/cmd/configDelete.go b/client/cmd/configDelete.go new file mode 100644 index 0000000..26a9b64 --- /dev/null +++ b/client/cmd/configDelete.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "log" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var configDeleteCmd = &cobra.Command{ + Use: "config-delete", + Short: "Delete a config option", + GroupID: GROUP_ID_CONFIG, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +var deleteCustomColumnsCmd = &cobra.Command{ + Use: "custom-columns", + Short: "Delete a custom column", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + columnName := args[0] + if config.CustomColumns == nil { + log.Fatalf("Did not find a column with name %#v to delete (current columns = %#v)", columnName, config.CustomColumns) + } + newColumns := make([]hctx.CustomColumnDefinition, 0) + deletedColumns := false + for _, c := range config.CustomColumns { + if c.ColumnName != columnName { + newColumns = append(newColumns, c) + deletedColumns = true + } + } + if !deletedColumns { + log.Fatalf("Did not find a column with name %#v to delete (current columns = %#v)", columnName, config.CustomColumns) + } + config.CustomColumns = newColumns + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} +var deleteDisplayedColumnCommand = &cobra.Command{ + Use: "displayed-columns", + Short: "Delete a displayed column", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + deletedColumns := args + newColumns := make([]string, 0) + for _, c := range config.DisplayedColumns { + isDeleted := false + for _, d := range deletedColumns { + if c == d { + isDeleted = true + } + } + if !isDeleted { + newColumns = append(newColumns, c) + } + } + config.DisplayedColumns = newColumns + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +func init() { + rootCmd.AddCommand(configDeleteCmd) + configDeleteCmd.AddCommand(deleteCustomColumnsCmd) + configDeleteCmd.AddCommand(deleteDisplayedColumnCommand) +} diff --git a/client/cmd/configGet.go b/client/cmd/configGet.go new file mode 100644 index 0000000..efefda9 --- /dev/null +++ b/client/cmd/configGet.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/spf13/cobra" +) + +var GROUP_ID_CONFIG string = "group_id_config" + +var configGetCmd = &cobra.Command{ + Use: "config-get", + Short: "Get the value of a config option", + GroupID: GROUP_ID_CONFIG, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +var getEnableControlRCmd = &cobra.Command{ + Use: "enable-control-r", + Short: "Whether hishtory replaces your shell's default control-r", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + fmt.Println(config.ControlRSearchEnabled) + }, +} + +var getFilterDuplicateCommandsCmd = &cobra.Command{ + Use: "filter-duplicate-commands", + Short: "Whether hishtory filters out duplicate commands when displaying your history", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + fmt.Println(config.FilterDuplicateCommands) + }, +} + +var getDisplayedColumnsCmd = &cobra.Command{ + Use: "displayed-columns", + Short: "The list of columns that hishtory displays", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + for _, col := range config.DisplayedColumns { + if strings.Contains(col, " ") { + fmt.Printf("%q ", col) + } else { + fmt.Print(col + " ") + } + } + fmt.Print("\n") + }, +} + +var getTimestampFormatCmd = &cobra.Command{ + Use: "timestamp-format", + Short: "The go format string to use for formatting the timestamp", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + fmt.Println(config.TimestampFormat) + }, +} + +var getCustomColumnsCmd = &cobra.Command{ + Use: "custom-columns", + Short: "The list of custom columns that hishtory is tracking", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + for _, cc := range config.CustomColumns { + fmt.Println(cc.ColumnName + ": " + cc.ColumnCommand) + } + }, +} + +func init() { + rootCmd.AddCommand(configGetCmd) + configGetCmd.AddCommand(getEnableControlRCmd) + configGetCmd.AddCommand(getFilterDuplicateCommandsCmd) + configGetCmd.AddCommand(getDisplayedColumnsCmd) + configGetCmd.AddCommand(getTimestampFormatCmd) + configGetCmd.AddCommand(getCustomColumnsCmd) +} diff --git a/client/cmd/configSet.go b/client/cmd/configSet.go new file mode 100644 index 0000000..fd9c39f --- /dev/null +++ b/client/cmd/configSet.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "log" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var configSetCmd = &cobra.Command{ + Use: "config-set", + Short: "Set the value of a config option", + GroupID: GROUP_ID_CONFIG, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +var setEnableControlRCmd = &cobra.Command{ + Use: "enable-control-r", + Short: "Whether hishtory replaces your shell's default control-r", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + ValidArgs: []string{"true", "false"}, + Run: func(cmd *cobra.Command, args []string) { + val := args[0] + if val != "true" && val != "false" { + log.Fatalf("Unexpected config value %s, must be one of: true, false", val) + } + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + config.ControlRSearchEnabled = (val == "true") + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +var setFilterDuplicateCommandsCmd = &cobra.Command{ + Use: "filter-duplicate-commands", + Short: "Whether hishtory filters out duplicate commands when displaying your history", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + ValidArgs: []string{"true", "false"}, + Run: func(cmd *cobra.Command, args []string) { + val := args[0] + if val != "true" && val != "false" { + log.Fatalf("Unexpected config value %s, must be one of: true, false", val) + } + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + config.FilterDuplicateCommands = (val == "true") + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +var setDisplayedColumnsCmd = &cobra.Command{ + Use: "displayed-columns", + Short: "The list of columns that hishtory displays", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + config.DisplayedColumns = args + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +var setTimestampFormatCmd = &cobra.Command{ + Use: "timestamp-format", + Short: "The go format string to use for formatting the timestamp", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + config.TimestampFormat = args[0] + lib.CheckFatalError(hctx.SetConfig(config)) + }, +} + +func init() { + rootCmd.AddCommand(configSetCmd) + configSetCmd.AddCommand(setEnableControlRCmd) + configSetCmd.AddCommand(setFilterDuplicateCommandsCmd) + configSetCmd.AddCommand(setDisplayedColumnsCmd) + configSetCmd.AddCommand(setTimestampFormatCmd) +} diff --git a/client/cmd/enableDisable.go b/client/cmd/enableDisable.go new file mode 100644 index 0000000..76fdfb7 --- /dev/null +++ b/client/cmd/enableDisable.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var enableCmd = &cobra.Command{ + Use: "enable", + Short: "Enable hiSHtory recording", + GroupID: GROUP_ID_CONFIG, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + lib.CheckFatalError(lib.Enable(ctx)) + }, +} + +var disableCmd = &cobra.Command{ + Use: "disable", + Short: "Disable hiSHtory recording", + GroupID: GROUP_ID_CONFIG, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + lib.CheckFatalError(lib.Disable(ctx)) + }, +} + +func init() { + rootCmd.AddCommand(enableCmd) + rootCmd.AddCommand(disableCmd) +} diff --git a/client/cmd/import.go b/client/cmd/import.go new file mode 100644 index 0000000..18d120a --- /dev/null +++ b/client/cmd/import.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "fmt" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var importCmd = &cobra.Command{ + Use: "import", + Hidden: true, + Short: "Re-import history entries from your existing shell history", + Long: "Note that you must pipe commands to be imported in via stdin. For example `history | hishtory import`.", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + numImported, err := lib.ImportHistory(ctx, true, true) + lib.CheckFatalError(err) + if numImported > 0 { + fmt.Printf("Imported %v history entries from your existing shell history\n", numImported) + } + }, +} + +func init() { + rootCmd.AddCommand(importCmd) +} diff --git a/client/cmd/install.go b/client/cmd/install.go new file mode 100644 index 0000000..9334a85 --- /dev/null +++ b/client/cmd/install.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var offlineInit *bool +var offlineInstall *bool + +var installCmd = &cobra.Command{ + Use: "install", + Hidden: true, + Short: "Copy this binary to ~/.hishtory/ and configure your shell to use it for recording your shell history", + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + secretKey := "" + if len(args) > 0 { + secretKey = args[0] + } + lib.CheckFatalError(lib.Install(secretKey, *offlineInstall)) + if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" { + db, err := hctx.OpenLocalSqliteDb() + lib.CheckFatalError(err) + data, err := lib.Search(nil, db, "", 10) + lib.CheckFatalError(err) + if len(data) < 10 { + fmt.Println("Importing existing shell history...") + ctx := hctx.MakeContext() + numImported, err := lib.ImportHistory(ctx, false, false) + lib.CheckFatalError(err) + if numImported > 0 { + fmt.Printf("Imported %v history entries from your existing shell history\n", numImported) + } + } + } + }, +} + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Re-initialize hiSHtory with a specified secret key", + GroupID: GROUP_ID_CONFIG, + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + db, err := hctx.OpenLocalSqliteDb() + lib.CheckFatalError(err) + data, err := lib.Search(nil, db, "", 10) + lib.CheckFatalError(err) + if len(data) > 0 { + fmt.Printf("Your current hishtory profile has saved history entries, are you sure you want to run `init` and reset?\nNote: This won't clear any imported history entries from your existing shell\n[y/N]") + reader := bufio.NewReader(os.Stdin) + resp, err := reader.ReadString('\n') + lib.CheckFatalError(err) + if strings.TrimSpace(resp) != "y" { + fmt.Printf("Aborting init per user response of %#v\n", strings.TrimSpace(resp)) + return + } + } + secretKey := "" + if len(args) > 0 { + secretKey = args[0] + } + lib.CheckFatalError(lib.Setup(secretKey, *offlineInit)) + if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" { + fmt.Println("Importing existing shell history...") + ctx := hctx.MakeContext() + numImported, err := lib.ImportHistory(ctx, false, false) + lib.CheckFatalError(err) + if numImported > 0 { + fmt.Printf("Imported %v history entries from your existing shell history\n", numImported) + } + } + }, +} + +func init() { + rootCmd.AddCommand(installCmd) + rootCmd.AddCommand(initCmd) + offlineInit = initCmd.Flags().Bool("offline", false, "Install hiSHtory in offline mode wiht all syncing capabilities disabled") + offlineInstall = installCmd.Flags().Bool("offline", false, "Install hiSHtory in offline mode wiht all syncing capabilities disabled") +} diff --git a/client/cmd/query.go b/client/cmd/query.go new file mode 100644 index 0000000..c00a3a1 --- /dev/null +++ b/client/cmd/query.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var EXAMPLE_QUERIES string = `Example queries: +'hishtory SUBCOMMAND apt-get' # Find shell commands containing 'apt-get' +'hishtory SUBCOMMAND apt-get install' # Find shell commands containing 'apt-get' and 'install' +'hishtory SUBCOMMAND curl cwd:/tmp/' # Find shell commands containing 'curl' run in '/tmp/' +'hishtory SUBCOMMAND curl user:david' # Find shell commands containing 'curl' run by 'david' +'hishtory SUBCOMMAND curl host:x1' # Find shell commands containing 'curl' run on 'x1' +'hishtory SUBCOMMAND exit_code:1' # Find shell commands that exited with status code 1 +'hishtory SUBCOMMAND before:2022-02-01' # Find shell commands run before 2022-02-01 +` + +var GROUP_ID_QUERYING string = "group_id:querying" + +var queryCmd = &cobra.Command{ + Use: "query", + Short: "Query your shell history and display the results in an ASCII art table", + GroupID: GROUP_ID_QUERYING, + Long: strings.ReplaceAll(EXAMPLE_QUERIES, "SUBCOMMAND", "query"), + DisableFlagParsing: true, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) + query(ctx, strings.Join(args, " ")) + }, +} + +var tqueryCmd = &cobra.Command{ + Use: "tquery", + Short: "Interactively query your shell history in a TUI interface", + GroupID: GROUP_ID_QUERYING, + Long: strings.ReplaceAll(EXAMPLE_QUERIES, "SUBCOMMAND", "tquery"), + DisableFlagParsing: true, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + lib.CheckFatalError(lib.TuiQuery(ctx, strings.Join(args, " "))) + }, +} + +var exportCmd = &cobra.Command{ + Use: "export", + Short: "Export your shell history and display just the raw commands", + GroupID: GROUP_ID_QUERYING, + Long: strings.ReplaceAll(EXAMPLE_QUERIES, "SUBCOMMAND", "export"), + DisableFlagParsing: true, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) + export(ctx, strings.Join(args, " ")) + }, +} + +func export(ctx *context.Context, query string) { + db := hctx.GetDb(ctx) + err := lib.RetrieveAdditionalEntriesFromRemote(ctx) + if err != nil { + if lib.IsOfflineError(err) { + fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!") + } else { + lib.CheckFatalError(err) + } + } + data, err := lib.Search(ctx, db, query, 0) + lib.CheckFatalError(err) + for i := len(data) - 1; i >= 0; i-- { + fmt.Println(data[i].Command) + } +} + +func query(ctx *context.Context, query string) { + db := hctx.GetDb(ctx) + err := lib.RetrieveAdditionalEntriesFromRemote(ctx) + if err != nil { + if lib.IsOfflineError(err) { + fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!") + } else { + lib.CheckFatalError(err) + } + } + lib.CheckFatalError(displayBannerIfSet(ctx)) + numResults := 25 + data, err := lib.Search(ctx, db, query, numResults*5) + lib.CheckFatalError(err) + lib.CheckFatalError(lib.DisplayResults(ctx, data, numResults)) +} + +func displayBannerIfSet(ctx *context.Context) error { + respBody, err := lib.GetBanner(ctx) + if lib.IsOfflineError(err) { + return nil + } + if err != nil { + return err + } + if len(respBody) > 0 { + fmt.Println(string(respBody)) + } + return nil +} + +func init() { + rootCmd.AddCommand(queryCmd) + rootCmd.AddCommand(tqueryCmd) + rootCmd.AddCommand(exportCmd) +} diff --git a/client/cmd/redact.go b/client/cmd/redact.go new file mode 100644 index 0000000..46f2b73 --- /dev/null +++ b/client/cmd/redact.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "os" + "strings" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var GROUP_ID_MANAGEMENT string = "group_id_management" + +var redactCmd = &cobra.Command{ + Use: "redact", + Aliases: []string{"delete"}, + Short: "Query for matching commands and remove them from your shell history", + Long: "This removes history entries on the current machine and on all remote machines. Supports the same query format as 'hishtory query'.", + GroupID: GROUP_ID_MANAGEMENT, + DisableFlagParsing: true, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + lib.CheckFatalError(lib.RetrieveAdditionalEntriesFromRemote(ctx)) + lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) + query := strings.Join(args, " ") + lib.CheckFatalError(lib.Redact(ctx, query, os.Getenv("HISHTORY_REDACT_FORCE") != "")) + }, +} + +func init() { + rootCmd.AddCommand(redactCmd) +} diff --git a/client/cmd/reupload.go b/client/cmd/reupload.go new file mode 100644 index 0000000..88af658 --- /dev/null +++ b/client/cmd/reupload.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var reuploadCmd = &cobra.Command{ + Use: "reupload", + Hidden: true, + Short: "[Debug Only] Reupload your entire hiSHtory to all other devices", + Run: func(cmd *cobra.Command, args []string) { + lib.CheckFatalError(lib.Reupload(hctx.MakeContext())) + }, +} + +func init() { + rootCmd.AddCommand(reuploadCmd) +} diff --git a/client/cmd/root.go b/client/cmd/root.go new file mode 100644 index 0000000..762ab94 --- /dev/null +++ b/client/cmd/root.go @@ -0,0 +1,33 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "os" + + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "hiSHtory", + Short: "hiSHtory: Better shell history", +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.AddGroup(&cobra.Group{ID: GROUP_ID_QUERYING, Title: "History Searching"}) + rootCmd.AddGroup(&cobra.Group{ID: GROUP_ID_MANAGEMENT, Title: "History Management"}) + rootCmd.AddGroup(&cobra.Group{ID: GROUP_ID_CONFIG, Title: "Configuration"}) + rootCmd.Version = "v0." + lib.Version +} diff --git a/client/cmd/saveHistoryEntry.go b/client/cmd/saveHistoryEntry.go new file mode 100644 index 0000000..c9fed2a --- /dev/null +++ b/client/cmd/saveHistoryEntry.go @@ -0,0 +1,140 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/ddworken/hishtory/client/data" + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/ddworken/hishtory/shared" + "github.com/spf13/cobra" +) + +var saveHistoryEntryCmd = &cobra.Command{ + Use: "saveHistoryEntry", + Hidden: true, + Short: "[Internal-only] The command used to save history entries", + DisableFlagParsing: true, + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + lib.CheckFatalError(maybeUploadSkippedHistoryEntries(ctx)) + saveHistoryEntry(ctx) + }, +} + +func maybeUploadSkippedHistoryEntries(ctx *context.Context) error { + config := hctx.GetConf(ctx) + if !config.HaveMissedUploads { + return nil + } + if config.IsOffline { + return nil + } + + // Upload the missing entries + db := hctx.GetDb(ctx) + query := fmt.Sprintf("after:%s", time.Unix(config.MissedUploadTimestamp, 0).Format("2006-01-02")) + entries, err := lib.Search(ctx, db, query, 0) + if err != nil { + return fmt.Errorf("failed to retrieve history entries that haven't been uploaded yet: %v", err) + } + hctx.GetLogger().Infof("Uploading %d history entries that previously failed to upload (query=%#v)\n", len(entries), query) + jsonValue, err := lib.EncryptAndMarshal(config, entries) + if err != nil { + return err + } + _, err = lib.ApiPost("/api/v1/submit?source_device_id="+config.DeviceId, "application/json", jsonValue) + if err != nil { + // Failed to upload the history entry, so we must still be offline. So just return nil and we'll try again later. + return nil + } + + // Mark down that we persisted it + config.HaveMissedUploads = false + config.MissedUploadTimestamp = 0 + err = hctx.SetConfig(config) + if err != nil { + return fmt.Errorf("failed to mark a history entry as uploaded: %v", err) + } + return nil +} + +func saveHistoryEntry(ctx *context.Context) { + config := hctx.GetConf(ctx) + if !config.IsEnabled { + hctx.GetLogger().Infof("Skipping saving a history entry because hishtory is disabled\n") + return + } + entry, err := lib.BuildHistoryEntry(ctx, os.Args) + lib.CheckFatalError(err) + if entry == nil { + hctx.GetLogger().Infof("Skipping saving a history entry because we did not build a history entry (was the command prefixed with a space and/or empty?)\n") + return + } + + // Persist it locally + db := hctx.GetDb(ctx) + err = lib.ReliableDbCreate(db, *entry) + lib.CheckFatalError(err) + + // Persist it remotely + if !config.IsOffline { + jsonValue, err := lib.EncryptAndMarshal(config, []*data.HistoryEntry{entry}) + lib.CheckFatalError(err) + _, err = lib.ApiPost("/api/v1/submit?source_device_id="+config.DeviceId, "application/json", jsonValue) + if err != nil { + if lib.IsOfflineError(err) { + hctx.GetLogger().Infof("Failed to remotely persist hishtory entry because we failed to connect to the remote server! This is likely because the device is offline, but also could be because the remote server is having reliability issues. Original error: %v", err) + if !config.HaveMissedUploads { + config.HaveMissedUploads = true + config.MissedUploadTimestamp = time.Now().Unix() + lib.CheckFatalError(hctx.SetConfig(config)) + } + } else { + lib.CheckFatalError(err) + } + } + } + + // Check if there is a pending dump request and reply to it if so + dumpRequests, err := lib.GetDumpRequests(config) + if err != nil { + if lib.IsOfflineError(err) { + // It is fine to just ignore this, the next command will retry the API and eventually we will respond to any pending dump requests + dumpRequests = []*shared.DumpRequest{} + hctx.GetLogger().Infof("Failed to check for dump requests because we failed to connect to the remote server!") + } else { + lib.CheckFatalError(err) + } + } + if len(dumpRequests) > 0 { + lib.CheckFatalError(lib.RetrieveAdditionalEntriesFromRemote(ctx)) + entries, err := lib.Search(ctx, db, "", 0) + lib.CheckFatalError(err) + var encEntries []*shared.EncHistoryEntry + for _, entry := range entries { + enc, err := data.EncryptHistoryEntry(config.UserSecret, *entry) + lib.CheckFatalError(err) + encEntries = append(encEntries, &enc) + } + reqBody, err := json.Marshal(encEntries) + lib.CheckFatalError(err) + for _, dumpRequest := range dumpRequests { + if !config.IsOffline { + _, err := lib.ApiPost("/api/v1/submit-dump?user_id="+dumpRequest.UserId+"&requesting_device_id="+dumpRequest.RequestingDeviceId+"&source_device_id="+config.DeviceId, "application/json", reqBody) + lib.CheckFatalError(err) + } + } + } + + // Handle deletion requests + lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) +} + +func init() { + rootCmd.AddCommand(saveHistoryEntryCmd) +} diff --git a/client/cmd/status.go b/client/cmd/status.go new file mode 100644 index 0000000..1dd42fd --- /dev/null +++ b/client/cmd/status.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "fmt" + + "github.com/ddworken/hishtory/client/data" + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var verbose *bool + +var statusCmd = &cobra.Command{ + Use: "status", + Short: "View status info including the secret key which is needed to sync shell history from another machine", + Run: func(cmd *cobra.Command, args []string) { + ctx := hctx.MakeContext() + config := hctx.GetConf(ctx) + fmt.Printf("hiSHtory: v0.%s\nEnabled: %v\n", lib.Version, config.IsEnabled) + fmt.Printf("Secret Key: %s\n", config.UserSecret) + if *verbose { + fmt.Printf("User ID: %s\n", data.UserId(config.UserSecret)) + fmt.Printf("Device ID: %s\n", config.DeviceId) + printDumpStatus(config) + } + fmt.Printf("Commit Hash: %s\n", lib.GitCommit) + }, +} + +func printDumpStatus(config hctx.ClientConfig) { + dumpRequests, err := lib.GetDumpRequests(config) + lib.CheckFatalError(err) + fmt.Printf("Dump Requests: ") + for _, d := range dumpRequests { + fmt.Printf("%#v, ", *d) + } + fmt.Print("\n") +} + +func init() { + rootCmd.AddCommand(statusCmd) + verbose = statusCmd.Flags().BoolP("verbose", "v", false, "Display verbose hiSHtory information") +} diff --git a/client/cmd/uninstall.go b/client/cmd/uninstall.go new file mode 100644 index 0000000..9bfcb02 --- /dev/null +++ b/client/cmd/uninstall.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "bufio" + "fmt" + "os" + "strings" + + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Completely uninstall hiSHtory and remove your shell history", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Are you sure you want to uninstall hiSHtory and delete all locally saved history data [y/N]") + reader := bufio.NewReader(os.Stdin) + resp, err := reader.ReadString('\n') + lib.CheckFatalError(err) + if strings.TrimSpace(resp) != "y" { + fmt.Printf("Aborting uninstall per user response of %#v\n", strings.TrimSpace(resp)) + return + } + lib.CheckFatalError(lib.Uninstall(hctx.MakeContext())) + }, +} + +func init() { + rootCmd.AddCommand(uninstallCmd) +} diff --git a/client/cmd/update.go b/client/cmd/update.go new file mode 100644 index 0000000..2974758 --- /dev/null +++ b/client/cmd/update.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/ddworken/hishtory/client/hctx" + "github.com/ddworken/hishtory/client/lib" + "github.com/spf13/cobra" +) + +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Securely update hishtory to the latest version", + Run: func(cmd *cobra.Command, args []string) { + lib.CheckFatalError(lib.Update(hctx.MakeContext())) + }, +} + +func init() { + rootCmd.AddCommand(updateCmd) +} diff --git a/client/hctx/hctx.go b/client/hctx/hctx.go index 9635fdb..74c6074 100644 --- a/client/hctx/hctx.go +++ b/client/hctx/hctx.go @@ -78,7 +78,7 @@ func OpenLocalSqliteDb() (*gorm.DB, error) { return nil, err } newLogger := logger.New( - GetLogger(), + GetLogger().WithField("fromSQL", true), logger.Config{ SlowThreshold: 100 * time.Millisecond, LogLevel: logger.Warn, diff --git a/client/lib/goldens/testCustomColumns-query-isAction=false b/client/lib/goldens/testCustomColumns-query-isAction=false index 8ceba6d..c5f0427 100644 --- a/client/lib/goldens/testCustomColumns-query-isAction=false +++ b/client/lib/goldens/testCustomColumns-query-isAction=false @@ -1,10 +1,10 @@ -Exit Code git_remote Command -0 git@github.com:ddworken/hishtory.git hishtory config-set displayed-columns 'Exit Code' git_remote Command -0 echo bar -0 cd / -0 git@github.com:ddworken/hishtory.git echo foo -0 git@github.com:ddworken/hishtory.git hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true' -0 echo baz -0 cd / -0 echo $FOOBAR world -0 export FOOBAR='hello' +Exit Code git_remote Command +0 git@github.com:ddworken/hishtory.git hishtory config-set displayed-columns 'Exit Code' git_remote Command +0 echo bar +0 cd / +0 git@github.com:ddworken/hishtory.git echo foo +0 git@github.com:ddworken/hishtory.git hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true' +0 echo baz +0 cd / +0 echo $FOOBAR world +0 export FOOBAR='hello' diff --git a/client/lib/goldens/testCustomColumns-query-isAction=true b/client/lib/goldens/testCustomColumns-query-isAction=true index f27ffea..ac6c291 100644 --- a/client/lib/goldens/testCustomColumns-query-isAction=true +++ b/client/lib/goldens/testCustomColumns-query-isAction=true @@ -3,7 +3,7 @@ Exit Code git_remote Command 0 echo bar 0 cd / 0 https://github.com/ddworken/hishtory echo foo -0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true' +0 https://github.com/ddworken/hishtory hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true' 0 echo baz 0 cd / 0 echo $FOOBAR world diff --git a/client/lib/goldens/testCustomColumns-tquery-bash b/client/lib/goldens/testCustomColumns-tquery-bash index e1718e7..e0b133d 100644 --- a/client/lib/goldens/testCustomColumns-tquery-bash +++ b/client/lib/goldens/testCustomColumns-tquery-bash @@ -5,27 +5,27 @@ bash-5.2$ hishtory tquery -pipefail Search Query: > -pipefail -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Exit Code git_remote Command │ -│─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ -│ 0 git@github.com:ddworken/hishtory.git hishtory config-set displayed-columns 'Exit Code' git_remote Command │ -│ 0 echo bar │ -│ 0 cd / │ -│ 0 git@github.com:ddworken/hishtory.git echo foo │ -│ 0 git@github.com:ddworken/hishtory.git hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ -│ 0 echo baz │ -│ 0 cd / │ -│ 0 echo $FOOBAR world │ -│ 0 export FOOBAR='hello' │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ \ No newline at end of file +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Exit Code git_remote Command │ +│──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ 0 git@github.com:ddworken/hishtory.git hishtory config-set displayed-columns 'Exit Code' git_remote Command │ +│ 0 echo bar │ +│ 0 cd / │ +│ 0 git@github.com:ddworken/hishtory.git echo foo │ +│ 0 git@github.com:ddworken/hishtory.git hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ +│ 0 echo baz │ +│ 0 cd / │ +│ 0 echo $FOOBAR world │ +│ 0 export FOOBAR='hello' │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/client/lib/goldens/testCustomColumns-tquery-bash-isAction-darwin b/client/lib/goldens/testCustomColumns-tquery-bash-isAction-darwin index bdb37ea..b0950bd 100644 --- a/client/lib/goldens/testCustomColumns-tquery-bash-isAction-darwin +++ b/client/lib/goldens/testCustomColumns-tquery-bash-isAction-darwin @@ -12,7 +12,7 @@ Search Query: > -pipefail │ 0 echo bar │ │ 0 cd / │ │ 0 https://github.com/ddworken/hishtory echo foo │ -│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ +│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ │ 0 echo baz │ │ 0 cd / │ │ 0 echo $FOOBAR world │ diff --git a/client/lib/goldens/testCustomColumns-tquery-bash-isAction-linux b/client/lib/goldens/testCustomColumns-tquery-bash-isAction-linux index 7b13c1f..601f1a8 100644 --- a/client/lib/goldens/testCustomColumns-tquery-bash-isAction-linux +++ b/client/lib/goldens/testCustomColumns-tquery-bash-isAction-linux @@ -12,7 +12,7 @@ Search Query: > -pipefail │ 0 echo bar │ │ 0 cd / │ │ 0 https://github.com/ddworken/hishtory echo foo │ -│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ +│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ │ 0 echo baz │ │ 0 cd / │ │ 0 echo $FOOBAR world │ diff --git a/client/lib/goldens/testCustomColumns-tquery-zsh b/client/lib/goldens/testCustomColumns-tquery-zsh index 90c1128..a64ddad 100644 --- a/client/lib/goldens/testCustomColumns-tquery-zsh +++ b/client/lib/goldens/testCustomColumns-tquery-zsh @@ -11,7 +11,7 @@ Search Query: > -pipefail │ 0 echo bar │ │ 0 cd / │ │ 0 git@github.com:ddworken/hishtory.git echo foo │ -│ 0 git@github.com:ddworken/hishtory.git hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ +│ 0 git@github.com:ddworken/hishtory.git hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ │ 0 echo baz │ │ 0 cd / │ │ 0 echo $FOOBAR world │ diff --git a/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-darwin b/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-darwin index 8d032a7..5d88da3 100644 --- a/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-darwin +++ b/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-darwin @@ -11,7 +11,7 @@ Search Query: > -pipefail │ 0 echo bar │ │ 0 cd / │ │ 0 https://github.com/ddworken/hishtory echo foo │ -│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ +│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ │ 0 echo baz │ │ 0 cd / │ │ 0 echo $FOOBAR world │ diff --git a/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-linux b/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-linux index 59cd668..bbacba1 100644 --- a/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-linux +++ b/client/lib/goldens/testCustomColumns-tquery-zsh-isAction-linux @@ -11,7 +11,7 @@ Search Query: > -pipefail │ 0 echo bar │ │ 0 cd / │ │ 0 https://github.com/ddworken/hishtory echo foo │ -│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ +│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-columns git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │ │ 0 echo baz │ │ 0 cd / │ │ 0 echo $FOOBAR world │ diff --git a/client/lib/lib.go b/client/lib/lib.go index 2202a1a..c0ef071 100644 --- a/client/lib/lib.go +++ b/client/lib/lib.go @@ -48,6 +48,7 @@ var ConfigZshContents string var ConfigFishContents string var Version string = "Unknown" +var GitCommit string = "Unknown" // 256KB ought to be enough for any reasonable cmd var maxSupportedLineLengthForImport = 256_000 @@ -333,18 +334,9 @@ func shouldSkipHiddenCommand(ctx *context.Context, historyLine string) (bool, er return false, nil } -func Setup(args []string) error { - userSecret := uuid.Must(uuid.NewRandom()).String() - isOffline := false - if len(args) > 2 && args[2] != "" { - if args[2] == "--offline" { - isOffline = true - } else { - if args[2][0] == '-' { - return fmt.Errorf("refusing to set user secret to %#v since it looks like a flag", args[2]) - } - userSecret = args[2] - } +func Setup(userSecret string, isOffline bool) error { + if userSecret == "" { + userSecret = uuid.Must(uuid.NewRandom()).String() } fmt.Println("Setting secret hishtory key to " + string(userSecret)) @@ -660,7 +652,7 @@ func readFileToArray(path string) ([]string, error) { return lines, nil } -func Install() error { +func Install(secretKey string, offline bool) error { homedir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get user's home directory: %v", err) @@ -692,7 +684,7 @@ func Install() error { _, err = hctx.GetConfig() if err != nil { // No config, so set up a new installation - return Setup(os.Args) + return Setup(secretKey, offline) } return nil } @@ -1470,12 +1462,12 @@ func ProcessDeletionRequests(ctx *context.Context) error { return nil } -func GetBanner(ctx *context.Context, gitCommit string) ([]byte, error) { +func GetBanner(ctx *context.Context) ([]byte, error) { config := hctx.GetConf(ctx) if config.IsOffline { return []byte{}, nil } - url := "/api/v1/banner?commit_hash=" + gitCommit + "&user_id=" + data.UserId(config.UserSecret) + "&device_id=" + config.DeviceId + "&version=" + Version + "&forced_banner=" + os.Getenv("FORCED_BANNER") + url := "/api/v1/banner?commit_hash=" + GitCommit + "&user_id=" + data.UserId(config.UserSecret) + "&device_id=" + config.DeviceId + "&version=" + Version + "&forced_banner=" + os.Getenv("FORCED_BANNER") return ApiGet(url) } @@ -1703,3 +1695,19 @@ func Uninstall(ctx *context.Context) error { fmt.Println("Successfully uninstalled hishtory, please restart your terminal...") return nil } + +func GetDumpRequests(config hctx.ClientConfig) ([]*shared.DumpRequest, error) { + if config.IsOffline { + return make([]*shared.DumpRequest, 0), nil + } + resp, err := ApiGet("/api/v1/get-dump-requests?user_id=" + data.UserId(config.UserSecret) + "&device_id=" + config.DeviceId) + if IsOfflineError(err) { + return []*shared.DumpRequest{}, nil + } + if err != nil { + return nil, err + } + var dumpRequests []*shared.DumpRequest + err = json.Unmarshal(resp, &dumpRequests) + return dumpRequests, err +} diff --git a/client/lib/lib_test.go b/client/lib/lib_test.go index a20ef45..4e0964a 100644 --- a/client/lib/lib_test.go +++ b/client/lib/lib_test.go @@ -23,7 +23,7 @@ func TestSetup(t *testing.T) { if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err == nil { t.Fatalf("hishtory secret file already exists!") } - testutils.Check(t, Setup([]string{})) + testutils.Check(t, Setup("", false)) if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err != nil { t.Fatalf("hishtory secret file does not exist after Setup()!") } @@ -47,7 +47,7 @@ func TestSetupOffline(t *testing.T) { if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err == nil { t.Fatalf("hishtory secret file already exists!") } - testutils.Check(t, Setup([]string{"", "", "--offline"})) + testutils.Check(t, Setup("", true)) if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err != nil { t.Fatalf("hishtory secret file does not exist after Setup()!") } @@ -65,7 +65,7 @@ func TestSetupOffline(t *testing.T) { func TestBuildHistoryEntry(t *testing.T) { defer testutils.BackupAndRestore(t)() defer testutils.RunTestServer()() - testutils.Check(t, Setup([]string{})) + testutils.Check(t, Setup("", false)) // Test building an actual entry for bash entry, err := BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", " 123 ls /foo ", "1641774958"}) @@ -158,7 +158,7 @@ func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) { defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")() defer testutils.BackupAndRestore(t)() defer testutils.RunTestServer()() - testutils.Check(t, Setup([]string{})) + testutils.Check(t, Setup("", false)) testcases := []struct { input, histtimeformat, expectedCommand string diff --git a/client/lib/tui.go b/client/lib/tui.go index 1b71675..fa4104e 100644 --- a/client/lib/tui.go +++ b/client/lib/tui.go @@ -408,7 +408,7 @@ func makeTable(ctx *context.Context, rows []table.Row) (table.Model, error) { return t, nil } -func TuiQuery(ctx *context.Context, gitCommit, initialQuery string) error { +func TuiQuery(ctx *context.Context, initialQuery string) error { lipgloss.SetColorProfile(termenv.ANSI) rows, numEntries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, initialQuery, PADDED_NUM_ENTRIES) if err != nil { @@ -435,7 +435,7 @@ func TuiQuery(ctx *context.Context, gitCommit, initialQuery string) error { }() // Async: Check for any banner from the server go func() { - banner, err := GetBanner(ctx, gitCommit) + banner, err := GetBanner(ctx) if err != nil { if IsOfflineError(err) { p.Send(offlineMsg{}) diff --git a/go.mod b/go.mod index 8ce2bea..788c506 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/PaesslerAG/gval v1.0.0 // indirect github.com/PaesslerAG/jsonpath v0.1.1 // indirect github.com/ThalesIgnite/crypto11 v1.2.5 // indirect + github.com/alecthomas/kong v0.7.1 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect @@ -211,7 +212,7 @@ require ( github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.6.0 // indirect + github.com/spf13/cobra v1.6.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.13.0 // indirect diff --git a/go.sum b/go.sum index 4975d1d..e74e074 100644 --- a/go.sum +++ b/go.sum @@ -226,6 +226,8 @@ github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= +github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= +github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -1955,6 +1957,8 @@ github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= diff --git a/hishtory.go b/hishtory.go index e689d59..66e5534 100644 --- a/hishtory.go +++ b/hishtory.go @@ -1,483 +1,11 @@ package main import ( - "bufio" - "context" - "encoding/json" - "fmt" - "log" - "os" - "strings" - "time" - - "github.com/ddworken/hishtory/client/data" - "github.com/ddworken/hishtory/client/hctx" - "github.com/ddworken/hishtory/client/lib" - "github.com/ddworken/hishtory/shared" + "github.com/ddworken/hishtory/client/cmd" ) -var GitCommit string = "Unknown" - func main() { - if len(os.Args) == 1 { - fmt.Println("Must specify a command! Do you mean `hishtory query`?") - return - } - switch os.Args[1] { - case "saveHistoryEntry": - ctx := hctx.MakeContext() - lib.CheckFatalError(maybeUploadSkippedHistoryEntries(ctx)) - saveHistoryEntry(ctx) - case "query": - ctx := hctx.MakeContext() - lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) - query(ctx, strings.Join(os.Args[2:], " ")) - case "tquery": - ctx := hctx.MakeContext() - lib.CheckFatalError(lib.TuiQuery(ctx, GitCommit, strings.Join(os.Args[2:], " "))) - case "export": - ctx := hctx.MakeContext() - lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) - export(ctx, strings.Join(os.Args[2:], " ")) - case "redact": - fallthrough - case "delete": - ctx := hctx.MakeContext() - lib.CheckFatalError(lib.RetrieveAdditionalEntriesFromRemote(ctx)) - lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) - query := strings.Join(os.Args[2:], " ") - force := false - if os.Args[2] == "--force" { - query = strings.Join(os.Args[3:], " ") - force = true - } - lib.CheckFatalError(lib.Redact(ctx, query, force)) - case "init": - db, err := hctx.OpenLocalSqliteDb() - lib.CheckFatalError(err) - data, err := lib.Search(nil, db, "", 10) - lib.CheckFatalError(err) - if len(data) > 0 { - fmt.Printf("Your current hishtory profile has saved history entries, are you sure you want to run `init` and reset?\nNote: This won't clear any imported history entries from your existing shell\n[y/N]") - reader := bufio.NewReader(os.Stdin) - resp, err := reader.ReadString('\n') - lib.CheckFatalError(err) - if strings.TrimSpace(resp) != "y" { - fmt.Printf("Aborting init per user response of %#v\n", strings.TrimSpace(resp)) - return - } - } - lib.CheckFatalError(lib.Setup(os.Args)) - if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" { - fmt.Println("Importing existing shell history...") - ctx := hctx.MakeContext() - numImported, err := lib.ImportHistory(ctx, false, false) - lib.CheckFatalError(err) - if numImported > 0 { - fmt.Printf("Imported %v history entries from your existing shell history\n", numImported) - } - } - case "install": - lib.CheckFatalError(lib.Install()) - if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" { - db, err := hctx.OpenLocalSqliteDb() - lib.CheckFatalError(err) - data, err := lib.Search(nil, db, "", 10) - lib.CheckFatalError(err) - if len(data) < 10 { - fmt.Println("Importing existing shell history...") - ctx := hctx.MakeContext() - numImported, err := lib.ImportHistory(ctx, false, false) - lib.CheckFatalError(err) - if numImported > 0 { - fmt.Printf("Imported %v history entries from your existing shell history\n", numImported) - } - } - } - case "uninstall": - fmt.Printf("Are you sure you want to uninstall hiSHtory and delete all locally saved history data [y/N]") - reader := bufio.NewReader(os.Stdin) - resp, err := reader.ReadString('\n') - lib.CheckFatalError(err) - if strings.TrimSpace(resp) != "y" { - fmt.Printf("Aborting uninstall per user response of %#v\n", strings.TrimSpace(resp)) - return - } - lib.CheckFatalError(lib.Uninstall(hctx.MakeContext())) - case "import": - ctx := hctx.MakeContext() - numImported, err := lib.ImportHistory(ctx, true, true) - lib.CheckFatalError(err) - if numImported > 0 { - fmt.Printf("Imported %v history entries from your existing shell history\n", numImported) - } - case "enable": - ctx := hctx.MakeContext() - lib.CheckFatalError(lib.Enable(ctx)) - case "disable": - ctx := hctx.MakeContext() - lib.CheckFatalError(lib.Disable(ctx)) - case "version": - fallthrough - case "status": - ctx := hctx.MakeContext() - config := hctx.GetConf(ctx) - fmt.Printf("hiSHtory: v0.%s\nEnabled: %v\n", lib.Version, config.IsEnabled) - fmt.Printf("Secret Key: %s\n", config.UserSecret) - if len(os.Args) == 3 && os.Args[2] == "-v" { - fmt.Printf("User ID: %s\n", data.UserId(config.UserSecret)) - fmt.Printf("Device ID: %s\n", config.DeviceId) - printDumpStatus(config) - } - fmt.Printf("Commit Hash: %s\n", GitCommit) - case "update": - err := lib.Update(hctx.MakeContext()) - if err != nil { - log.Fatalf("Failed to update hishtory: %v", err) - } - case "config-get": - ctx := hctx.MakeContext() - config := hctx.GetConf(ctx) - key := os.Args[2] - 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, " ") { - fmt.Printf("%q ", col) - } else { - fmt.Print(col + " ") - } - } - fmt.Print("\n") - case "custom-columns": - for _, cc := range config.CustomColumns { - fmt.Println(cc.ColumnName + ": " + cc.ColumnCommand) - } - default: - log.Fatalf("Unrecognized config key: %s", key) - } - case "config-set": - ctx := hctx.MakeContext() - config := hctx.GetConf(ctx) - key := os.Args[2] - switch key { - case "enable-control-r": - val := os.Args[3] - if val != "true" && val != "false" { - log.Fatalf("Unexpected config value %s, must be one of: true, false", val) - } - 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 - lib.CheckFatalError(hctx.SetConfig(config)) - case "timestamp-format": - val := os.Args[3] - config.TimestampFormat = val - lib.CheckFatalError(hctx.SetConfig(config)) - case "custom-columns": - log.Fatalf("Please use config-add and config-delete to interact with custom-columns") - default: - log.Fatalf("Unrecognized config key: %s", key) - } - case "config-add": - ctx := hctx.MakeContext() - config := hctx.GetConf(ctx) - key := os.Args[2] - switch key { - case "custom-column": - fallthrough - case "custom-columns": - columnName := os.Args[3] - command := os.Args[4] - ctx := hctx.MakeContext() - config := hctx.GetConf(ctx) - if config.CustomColumns == nil { - config.CustomColumns = make([]hctx.CustomColumnDefinition, 0) - } - config.CustomColumns = append(config.CustomColumns, hctx.CustomColumnDefinition{ColumnName: columnName, ColumnCommand: command}) - lib.CheckFatalError(hctx.SetConfig(config)) - case "displayed-columns": - vals := os.Args[3:] - config.DisplayedColumns = append(config.DisplayedColumns, vals...) - lib.CheckFatalError(hctx.SetConfig(config)) - default: - log.Fatalf("Unrecognized config key: %s", key) - } - case "config-delete": - ctx := hctx.MakeContext() - config := hctx.GetConf(ctx) - key := os.Args[2] - switch key { - case "custom-columns": - columnName := os.Args[2] - ctx := hctx.MakeContext() - config := hctx.GetConf(ctx) - if config.CustomColumns == nil { - return - } - newColumns := make([]hctx.CustomColumnDefinition, 0) - deletedColumns := false - for _, c := range config.CustomColumns { - if c.ColumnName != columnName { - newColumns = append(newColumns, c) - deletedColumns = true - } - } - if !deletedColumns { - log.Fatalf("Did not find a column with name %#v to delete (current columns = %#v)", columnName, config.CustomColumns) - } - config.CustomColumns = newColumns - lib.CheckFatalError(hctx.SetConfig(config)) - case "displayed-columns": - deletedColumns := os.Args[3:] - newColumns := make([]string, 0) - for _, c := range config.DisplayedColumns { - isDeleted := false - for _, d := range deletedColumns { - if c == d { - isDeleted = true - } - } - if !isDeleted { - newColumns = append(newColumns, c) - } - } - config.DisplayedColumns = newColumns - lib.CheckFatalError(hctx.SetConfig(config)) - default: - log.Fatalf("Unrecognized config key: %s", key) - } - case "reupload": - // Purposefully undocumented since this command is generally not necessary to run - lib.CheckFatalError(lib.Reupload(hctx.MakeContext())) - case "-h": - fallthrough - case "help": - fmt.Print(`hiSHtory: Better shell history - -Supported commands: - 'hishtory query': Query for matching commands and display them in a table. Examples: - 'hishtory query apt-get' # Find shell commands containing 'apt-get' - 'hishtory query apt-get install' # Find shell commands containing 'apt-get' and 'install' - 'hishtory query curl cwd:/tmp/' # Find shell commands containing 'curl' run in '/tmp/' - 'hishtory query curl user:david' # Find shell commands containing 'curl' run by 'david' - 'hishtory query curl host:x1' # Find shell commands containing 'curl' run on 'x1' - 'hishtory query exit_code:1' # Find shell commands that exited with status code 1 - 'hishtory query before:2022-02-01' # Find shell commands run before 2022-02-01 - 'hishtory export': Query for matching commands and display them in list without any other - metadata. Supports the same query format as 'hishtory query'. - 'hishtory redact': Query for matching commands and remove them from your shell history (on the - current machine and on all remote machines). Supports the same query format as 'hishtory query'. - 'hishtory update': Securely update hishtory to the latest version. - 'hishtory disable': Stop recording shell commands - 'hishtory enable': Start recording shell commands - 'hishtory status': View status info including the secret key which is needed to sync shell - history from another machine. - 'hishtory init': Set the secret key to enable syncing shell commands from another - machine with a matching secret key. - 'hishtory config-get', 'hishtory config-set', 'hishtory config-add', 'hishtory config-delete': Edit the config. See the README for details on each of the config options. - 'hishtory uninstall': Permanently uninstall hishtory - 'hishtory help': View this help page - `) - default: - lib.CheckFatalError(fmt.Errorf("unknown command: %s", os.Args[1])) - } -} - -func printDumpStatus(config hctx.ClientConfig) { - dumpRequests, err := getDumpRequests(config) - lib.CheckFatalError(err) - fmt.Printf("Dump Requests: ") - for _, d := range dumpRequests { - fmt.Printf("%#v, ", *d) - } - fmt.Print("\n") -} - -func getDumpRequests(config hctx.ClientConfig) ([]*shared.DumpRequest, error) { - if config.IsOffline { - return make([]*shared.DumpRequest, 0), nil - } - resp, err := lib.ApiGet("/api/v1/get-dump-requests?user_id=" + data.UserId(config.UserSecret) + "&device_id=" + config.DeviceId) - if lib.IsOfflineError(err) { - return []*shared.DumpRequest{}, nil - } - if err != nil { - return nil, err - } - var dumpRequests []*shared.DumpRequest - err = json.Unmarshal(resp, &dumpRequests) - return dumpRequests, err -} - -func query(ctx *context.Context, query string) { - db := hctx.GetDb(ctx) - err := lib.RetrieveAdditionalEntriesFromRemote(ctx) - if err != nil { - if lib.IsOfflineError(err) { - fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!") - } else { - lib.CheckFatalError(err) - } - } - lib.CheckFatalError(displayBannerIfSet(ctx)) - numResults := 25 - data, err := lib.Search(ctx, db, query, numResults*5) - lib.CheckFatalError(err) - lib.CheckFatalError(lib.DisplayResults(ctx, data, numResults)) -} - -func displayBannerIfSet(ctx *context.Context) error { - respBody, err := lib.GetBanner(ctx, GitCommit) - if lib.IsOfflineError(err) { - return nil - } - if err != nil { - return err - } - if len(respBody) > 0 { - fmt.Println(string(respBody)) - } - return nil -} - -func maybeUploadSkippedHistoryEntries(ctx *context.Context) error { - config := hctx.GetConf(ctx) - if !config.HaveMissedUploads { - return nil - } - if config.IsOffline { - return nil - } - - // Upload the missing entries - db := hctx.GetDb(ctx) - query := fmt.Sprintf("after:%s", time.Unix(config.MissedUploadTimestamp, 0).Format("2006-01-02")) - entries, err := lib.Search(ctx, db, query, 0) - if err != nil { - return fmt.Errorf("failed to retrieve history entries that haven't been uploaded yet: %v", err) - } - hctx.GetLogger().Infof("Uploading %d history entries that previously failed to upload (query=%#v)\n", len(entries), query) - jsonValue, err := lib.EncryptAndMarshal(config, entries) - if err != nil { - return err - } - _, err = lib.ApiPost("/api/v1/submit?source_device_id="+config.DeviceId, "application/json", jsonValue) - if err != nil { - // Failed to upload the history entry, so we must still be offline. So just return nil and we'll try again later. - return nil - } - - // Mark down that we persisted it - config.HaveMissedUploads = false - config.MissedUploadTimestamp = 0 - err = hctx.SetConfig(config) - if err != nil { - return fmt.Errorf("failed to mark a history entry as uploaded: %v", err) - } - return nil -} - -func saveHistoryEntry(ctx *context.Context) { - config := hctx.GetConf(ctx) - if !config.IsEnabled { - hctx.GetLogger().Infof("Skipping saving a history entry because hishtory is disabled\n") - return - } - entry, err := lib.BuildHistoryEntry(ctx, os.Args) - lib.CheckFatalError(err) - if entry == nil { - hctx.GetLogger().Infof("Skipping saving a history entry because we did not build a history entry (was the command prefixed with a space and/or empty?)\n") - return - } - - // Persist it locally - db := hctx.GetDb(ctx) - err = lib.ReliableDbCreate(db, *entry) - lib.CheckFatalError(err) - - // Persist it remotely - if !config.IsOffline { - jsonValue, err := lib.EncryptAndMarshal(config, []*data.HistoryEntry{entry}) - lib.CheckFatalError(err) - _, err = lib.ApiPost("/api/v1/submit?source_device_id="+config.DeviceId, "application/json", jsonValue) - if err != nil { - if lib.IsOfflineError(err) { - hctx.GetLogger().Infof("Failed to remotely persist hishtory entry because we failed to connect to the remote server! This is likely because the device is offline, but also could be because the remote server is having reliability issues. Original error: %v", err) - if !config.HaveMissedUploads { - config.HaveMissedUploads = true - config.MissedUploadTimestamp = time.Now().Unix() - lib.CheckFatalError(hctx.SetConfig(config)) - } - } else { - lib.CheckFatalError(err) - } - } - } - - // Check if there is a pending dump request and reply to it if so - dumpRequests, err := getDumpRequests(config) - if err != nil { - if lib.IsOfflineError(err) { - // It is fine to just ignore this, the next command will retry the API and eventually we will respond to any pending dump requests - dumpRequests = []*shared.DumpRequest{} - hctx.GetLogger().Infof("Failed to check for dump requests because we failed to connect to the remote server!") - } else { - lib.CheckFatalError(err) - } - } - if len(dumpRequests) > 0 { - lib.CheckFatalError(lib.RetrieveAdditionalEntriesFromRemote(ctx)) - entries, err := lib.Search(ctx, db, "", 0) - lib.CheckFatalError(err) - var encEntries []*shared.EncHistoryEntry - for _, entry := range entries { - enc, err := data.EncryptHistoryEntry(config.UserSecret, *entry) - lib.CheckFatalError(err) - encEntries = append(encEntries, &enc) - } - reqBody, err := json.Marshal(encEntries) - lib.CheckFatalError(err) - for _, dumpRequest := range dumpRequests { - if !config.IsOffline { - _, err := lib.ApiPost("/api/v1/submit-dump?user_id="+dumpRequest.UserId+"&requesting_device_id="+dumpRequest.RequestingDeviceId+"&source_device_id="+config.DeviceId, "application/json", reqBody) - lib.CheckFatalError(err) - } - } - } - - // Handle deletion requests - lib.CheckFatalError(lib.ProcessDeletionRequests(ctx)) -} - -func export(ctx *context.Context, query string) { - db := hctx.GetDb(ctx) - err := lib.RetrieveAdditionalEntriesFromRemote(ctx) - if err != nil { - if lib.IsOfflineError(err) { - fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!") - } else { - lib.CheckFatalError(err) - } - } - data, err := lib.Search(ctx, db, query, 0) - lib.CheckFatalError(err) - for i := len(data) - 1; i >= 0; i-- { - fmt.Println(data[i].Command) - } + cmd.Execute() } // TODO(feature): Add a session_id column that corresponds to the shell session the command was run in diff --git a/scripts/client-ldflags b/scripts/client-ldflags index 4097ec9..9cbea64 100755 --- a/scripts/client-ldflags +++ b/scripts/client-ldflags @@ -1,4 +1,4 @@ #!/usr/bin/env bash GIT_HASH=$(git rev-parse HEAD) -echo "-X main.GitCommit=$GIT_HASH -X github.com/ddworken/hishtory/client/lib.Version=`cat VERSION` -w -extldflags \"-static\"" +echo "-X github.com/ddworken/hishtory/client/lib.GitCommit=$GIT_HASH -X github.com/ddworken/hishtory/client/lib.Version=`cat VERSION` -w -extldflags \"-static\"" diff --git a/shared/testutils/testutils.go b/shared/testutils/testutils.go index 6ca8731..7166942 100644 --- a/shared/testutils/testutils.go +++ b/shared/testutils/testutils.go @@ -25,10 +25,8 @@ const ( func ResetLocalState(t *testing.T) { homedir, err := os.UserHomeDir() - if err != nil { - t.Fatalf("failed to retrieve homedir: %v", err) - } - + Check(t, err) + persistLog() _ = BackupAndRestoreWithId(t, "-reset-local-state") _ = os.RemoveAll(path.Join(homedir, data.HISHTORY_PATH)) } @@ -90,6 +88,7 @@ func BackupAndRestoreWithId(t *testing.T, id string) func() { t.Fatalf("failed to execute killall hishtory, stdout=%#v: %v", string(stdout), err) } } + persistLog() Check(t, os.RemoveAll(path.Join(homedir, data.HISHTORY_PATH))) Check(t, os.MkdirAll(path.Join(homedir, data.HISHTORY_PATH), os.ModePerm)) for _, file := range renameFiles { @@ -298,3 +297,20 @@ func TestLog(t *testing.T, line string) { Check(t, err) } } + +func persistLog() { + homedir, err := os.UserHomeDir() + checkError(err) + fp := path.Join(homedir, data.HISHTORY_PATH, "hishtory.log") + log, err := os.ReadFile(fp) + if err != nil { + return + } + f, err := os.OpenFile("/tmp/hishtory.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + checkError(err) + defer f.Close() + _, err = f.Write(log) + checkError(err) + _, err = f.WriteString("\n") + checkError(err) +}