Merge pull request #36 from ddworken/cobra

Migrate to cobra for CLI parsing
This commit is contained in:
David Dworken 2022-11-16 20:47:06 -08:00 committed by GitHub
commit 666d9c6e81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1009 additions and 564 deletions

View File

@ -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
```
</details>

View File

@ -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]
}

View File

@ -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

52
client/cmd/configAdd.go Normal file
View File

@ -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)
}

View File

@ -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)
}

89
client/cmd/configGet.go Normal file
View File

@ -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)
}

84
client/cmd/configSet.go Normal file
View File

@ -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)
}

View File

@ -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)
}

28
client/cmd/import.go Normal file
View File

@ -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)
}

88
client/cmd/install.go Normal file
View File

@ -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")
}

115
client/cmd/query.go Normal file
View File

@ -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)
}

32
client/cmd/redact.go Normal file
View File

@ -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)
}

20
client/cmd/reupload.go Normal file
View File

@ -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)
}

33
client/cmd/root.go Normal file
View File

@ -0,0 +1,33 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
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
}

View File

@ -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)
}

44
client/cmd/status.go Normal file
View File

@ -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")
}

32
client/cmd/uninstall.go Normal file
View File

@ -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)
}

19
client/cmd/update.go Normal file
View File

@ -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)
}

View File

@ -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,

View File

@ -3,7 +3,7 @@ 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 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

View File

@ -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

View File

@ -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' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ 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'
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

View File

@ -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 │

View File

@ -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 │

View File

@ -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 │

View File

@ -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 │

View File

@ -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 │

View File

@ -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
}

View File

@ -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

View File

@ -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{})

3
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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\""

View File

@ -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)
}