Add HomeDirectory to HistoryEntry so we can query with or without ~/ in the cwd atom

This commit is contained in:
David Dworken 2022-09-07 23:20:31 -07:00
parent e063f34997
commit d54bece705
4 changed files with 65 additions and 29 deletions

View File

@ -529,6 +529,24 @@ hishtory disable`)
if strings.Count(out, "\n") != 4 { if strings.Count(out, "\n") != 4 {
t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out) t.Fatalf("hishtory query has the wrong number of lines=%d, out=%#v", strings.Count(out, "\n"), out)
} }
// Search for a cwd based on the home directory
entry = data.MakeFakeHistoryEntry("foobar")
entry.HomeDirectory = "/home/david/"
entry.CurrentWorkingDirectory = "~/dir/"
manuallySubmitHistoryEntry(t, userSecret, entry)
out = tester.RunInteractiveShell(t, `hishtory export cwd:~/dir`)
expectedOutput := "foobar\n"
if diff := cmp.Diff(expectedOutput, out); diff != "" {
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
}
// And search with the fully expanded path
out = tester.RunInteractiveShell(t, `hishtory export cwd:/home/david/dir`)
expectedOutput = "foobar\n"
if diff := cmp.Diff(expectedOutput, out); diff != "" {
t.Fatalf("hishtory export mismatch (-expected +got):\n%s\nout=%#v", diff, out)
}
} }
func testUpdate(t *testing.T, tester shellTester) { func testUpdate(t *testing.T, tester shellTester) {

View File

@ -30,6 +30,7 @@ type HistoryEntry struct {
Hostname string `json:"hostname" gorm:"uniqueIndex:compositeindex"` Hostname string `json:"hostname" gorm:"uniqueIndex:compositeindex"`
Command string `json:"command" gorm:"uniqueIndex:compositeindex"` Command string `json:"command" gorm:"uniqueIndex:compositeindex"`
CurrentWorkingDirectory string `json:"current_working_directory" gorm:"uniqueIndex:compositeindex"` CurrentWorkingDirectory string `json:"current_working_directory" gorm:"uniqueIndex:compositeindex"`
HomeDirectory string `json:"home_directory" gorm:"uniqueIndex:compositeindex"`
ExitCode int `json:"exit_code" gorm:"uniqueIndex:compositeindex"` ExitCode int `json:"exit_code" gorm:"uniqueIndex:compositeindex"`
StartTime time.Time `json:"start_time" gorm:"uniqueIndex:compositeindex"` StartTime time.Time `json:"start_time" gorm:"uniqueIndex:compositeindex"`
EndTime time.Time `json:"end_time" gorm:"uniqueIndex:compositeindex"` EndTime time.Time `json:"end_time" gorm:"uniqueIndex:compositeindex"`
@ -141,11 +142,11 @@ func Search(db *gorm.DB, query string, limit int) ([]*HistoryEntry, error) {
for _, token := range tokens { for _, token := range tokens {
if strings.HasPrefix(token, "-") { if strings.HasPrefix(token, "-") {
if strings.Contains(token, ":") { if strings.Contains(token, ":") {
query, val, err := parseAtomizedToken(token[1:]) query, v1, v2, err := parseAtomizedToken(token[1:])
if err != nil { if err != nil {
return nil, err return nil, err
} }
tx = tx.Where("NOT "+query, val) tx = tx.Where("NOT "+query, v1, v2)
} else { } else {
query, v1, v2, v3, err := parseNonAtomizedToken(token[1:]) query, v1, v2, v3, err := parseNonAtomizedToken(token[1:])
if err != nil { if err != nil {
@ -154,11 +155,11 @@ func Search(db *gorm.DB, query string, limit int) ([]*HistoryEntry, error) {
tx = tx.Where("NOT "+query, v1, v2, v3) tx = tx.Where("NOT "+query, v1, v2, v3)
} }
} else if strings.Contains(token, ":") { } else if strings.Contains(token, ":") {
query, val, err := parseAtomizedToken(token) query, v1, v2, err := parseAtomizedToken(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tx = tx.Where(query, val) tx = tx.Where(query, v1, v2)
} else { } else {
query, v1, v2, v3, err := parseNonAtomizedToken(token) query, v1, v2, v3, err := parseNonAtomizedToken(token)
if err != nil { if err != nil {
@ -184,36 +185,35 @@ func parseNonAtomizedToken(token string) (string, interface{}, interface{}, inte
return "(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken, nil return "(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken, nil
} }
func parseAtomizedToken(token string) (string, interface{}, error) { func parseAtomizedToken(token string) (string, interface{}, interface{}, error) {
splitToken := strings.SplitN(token, ":", 2) splitToken := strings.SplitN(token, ":", 2)
field := splitToken[0] field := splitToken[0]
val := splitToken[1] val := splitToken[1]
switch field { switch field {
case "user": case "user":
return "(local_username = ?)", val, nil return "(local_username = ?)", val, nil, nil
case "host": case "host":
fallthrough fallthrough
case "hostname": case "hostname":
return "(instr(hostname, ?) > 0)", val, nil return "(instr(hostname, ?) > 0)", val, nil, nil
case "cwd": case "cwd":
// TODO: Can I make this support querying via ~/ too? return "(instr(current_working_directory, ?) > 0 OR instr(REPLACE(current_working_directory, '~/', home_directory), ?) > 0)", strings.TrimSuffix(val, "/"), strings.TrimSuffix(val, "/"), nil
return "(instr(current_working_directory, ?) > 0)", strings.TrimSuffix(val, "/"), nil
case "exit_code": case "exit_code":
return "(exit_code = ?)", val, nil return "(exit_code = ?)", val, nil, nil
case "before": case "before":
t, err := parseTimeGenerously(val) t, err := parseTimeGenerously(val)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("failed to parse before:%s as a timestamp: %v", val, err) return "", nil, nil, fmt.Errorf("failed to parse before:%s as a timestamp: %v", val, err)
} }
return "(CAST(strftime(\"%s\",start_time) AS INTEGER) < ?)", t.Unix(), nil return "(CAST(strftime(\"%s\",start_time) AS INTEGER) < ?)", t.Unix(), nil, nil
case "after": case "after":
t, err := parseTimeGenerously(val) t, err := parseTimeGenerously(val)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("failed to parse after:%s as a timestamp: %v", val, err) return "", nil, nil, fmt.Errorf("failed to parse after:%s as a timestamp: %v", val, err)
} }
return "(CAST(strftime(\"%s\",start_time) AS INTEGER) > ?)", t.Unix(), nil return "(CAST(strftime(\"%s\",start_time) AS INTEGER) > ?)", t.Unix(), nil, nil
default: default:
return "", nil, fmt.Errorf("search query contains unknown search atom %s", field) return "", nil, nil, fmt.Errorf("search query contains unknown search atom %s", field)
} }
} }
@ -229,6 +229,7 @@ func EntryEquals(entry1, entry2 HistoryEntry) bool {
entry1.Hostname == entry2.Hostname && entry1.Hostname == entry2.Hostname &&
entry1.Command == entry2.Command && entry1.Command == entry2.Command &&
entry1.CurrentWorkingDirectory == entry2.CurrentWorkingDirectory && entry1.CurrentWorkingDirectory == entry2.CurrentWorkingDirectory &&
entry1.HomeDirectory == entry2.HomeDirectory &&
entry1.ExitCode == entry2.ExitCode && entry1.ExitCode == entry2.ExitCode &&
entry1.StartTime.Format(time.RFC3339) == entry2.StartTime.Format(time.RFC3339) && entry1.StartTime.Format(time.RFC3339) == entry2.StartTime.Format(time.RFC3339) &&
entry1.EndTime.Format(time.RFC3339) == entry2.EndTime.Format(time.RFC3339) entry1.EndTime.Format(time.RFC3339) == entry2.EndTime.Format(time.RFC3339)
@ -240,6 +241,7 @@ func MakeFakeHistoryEntry(command string) HistoryEntry {
Hostname: "localhost", Hostname: "localhost",
Command: command, Command: command,
CurrentWorkingDirectory: "/tmp/", CurrentWorkingDirectory: "/tmp/",
HomeDirectory: "/home/david/",
ExitCode: 2, ExitCode: 2,
StartTime: time.Now(), StartTime: time.Now(),
EndTime: time.Now(), EndTime: time.Now(),

View File

@ -50,22 +50,22 @@ var TestConfigZshContents string
var Version string = "Unknown" var Version string = "Unknown"
func getCwd() (string, error) { func getCwd() (string, string, error) {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get cwd for last command: %v", err) return "", "", fmt.Errorf("failed to get cwd for last command: %v", err)
} }
homedir, err := os.UserHomeDir() homedir, err := os.UserHomeDir()
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get user's home directory: %v", err) return "", "", fmt.Errorf("failed to get user's home directory: %v", err)
} }
if cwd == homedir { if cwd == homedir {
return "~/", nil return "~/", homedir, nil
} }
if strings.HasPrefix(cwd, homedir) { if strings.HasPrefix(cwd, homedir) {
return strings.Replace(cwd, homedir, "~", 1), nil return strings.Replace(cwd, homedir, "~", 1), homedir, nil
} }
return cwd, nil return cwd, homedir, nil
} }
func BuildHistoryEntry(args []string) (*data.HistoryEntry, error) { func BuildHistoryEntry(args []string) (*data.HistoryEntry, error) {
@ -91,12 +91,13 @@ func BuildHistoryEntry(args []string) (*data.HistoryEntry, error) {
} }
entry.LocalUsername = user.Username entry.LocalUsername = user.Username
// cwd // cwd and homedir
cwd, err := getCwd() cwd, homedir, err := getCwd()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to build history entry: %v", err) return nil, fmt.Errorf("failed to build history entry: %v", err)
} }
entry.CurrentWorkingDirectory = cwd entry.CurrentWorkingDirectory = cwd
entry.HomeDirectory = homedir
// start time // start time
seconds, err := parseCrossPlatformInt(args[5]) seconds, err := parseCrossPlatformInt(args[5])
@ -122,7 +123,11 @@ func BuildHistoryEntry(args []string) (*data.HistoryEntry, error) {
// Don't save commands that start with a space // Don't save commands that start with a space
return nil, nil return nil, nil
} }
entry.Command = maybeSkipBashHistTimePrefix(cmd) cmd, err = maybeSkipBashHistTimePrefix(cmd)
if err != nil {
return nil, err
}
entry.Command = cmd
} else if shell == "zsh" { } else if shell == "zsh" {
cmd := strings.TrimSuffix(strings.TrimSuffix(args[4], "\n"), " ") cmd := strings.TrimSuffix(strings.TrimSuffix(args[4], "\n"), " ")
if strings.HasPrefix(cmd, " ") { if strings.HasPrefix(cmd, " ") {
@ -214,16 +219,16 @@ func buildRegexFromTimeFormat(timeFormat string) string {
return expectedRegex return expectedRegex
} }
func maybeSkipBashHistTimePrefix(cmdLine string) string { func maybeSkipBashHistTimePrefix(cmdLine string) (string, error) {
format := os.Getenv("HISTTIMEFORMAT") format := os.Getenv("HISTTIMEFORMAT")
if format == "" { if format == "" {
return cmdLine return cmdLine, nil
} }
re, err := regexp.Compile("^" + buildRegexFromTimeFormat(format)) re, err := regexp.Compile("^" + buildRegexFromTimeFormat(format))
if err != nil { if err != nil {
panic("TODO: bubble up this error") return "", fmt.Errorf("failed to parse regex for HISTTIMEFORMAT variable: %v", err)
} }
return re.ReplaceAllLiteralString(cmdLine, "") return re.ReplaceAllLiteralString(cmdLine, ""), nil
} }
func parseCrossPlatformInt(data string) (int64, error) { func parseCrossPlatformInt(data string) (int64, error) {
@ -314,6 +319,7 @@ func AddToDbIfNew(db *gorm.DB, entry data.HistoryEntry) {
tx = tx.Where("hostname = ?", entry.Hostname) tx = tx.Where("hostname = ?", entry.Hostname)
tx = tx.Where("command = ?", entry.Command) tx = tx.Where("command = ?", entry.Command)
tx = tx.Where("current_working_directory = ?", entry.CurrentWorkingDirectory) tx = tx.Where("current_working_directory = ?", entry.CurrentWorkingDirectory)
tx = tx.Where("home_directory = ?", entry.HomeDirectory)
tx = tx.Where("exit_code = ?", entry.ExitCode) tx = tx.Where("exit_code = ?", entry.ExitCode)
tx = tx.Where("start_time = ?", entry.StartTime) tx = tx.Where("start_time = ?", entry.StartTime)
tx = tx.Where("end_time = ?", entry.EndTime) tx = tx.Where("end_time = ?", entry.EndTime)

View File

@ -52,6 +52,9 @@ func TestBuildHistoryEntry(t *testing.T) {
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") { if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory) t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
} }
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" { if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command) t.Fatalf("history entry has unexpected command: %v", entry.Command)
} }
@ -74,6 +77,9 @@ func TestBuildHistoryEntry(t *testing.T) {
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") { if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory) t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
} }
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" { if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command) t.Fatalf("history entry has unexpected command: %v", entry.Command)
} }
@ -243,7 +249,11 @@ func TestMaybeSkipBashHistTimePrefix(t *testing.T) {
for _, tc := range testcases { for _, tc := range testcases {
os.Setenv("HISTTIMEFORMAT", tc.env) os.Setenv("HISTTIMEFORMAT", tc.env)
if stripped := maybeSkipBashHistTimePrefix(tc.cmdLine); stripped != tc.expected { stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine)
if err != nil {
t.Fatal(err)
}
if stripped != tc.expected {
t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine) t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine)
} }
} }