diff --git a/CHANGELOG.md b/CHANGELOG.md index c978703..b54572a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v0.3.5 +### Features +- **Path Updates**: Added new `update-path` action to update directory paths in history entries +- Support for both exact path updates and subdirectory path updates +- Automatic path standardization (absolute paths, cleanup) + ## v0.3.4 ### Bug Fixes - Fixed incorrect URLs in documentation and package references diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..31e6622 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,22 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands +- Build: `make` or `go build -ldflags "-X main.Version=$(VERSION)" -o bin/histree-core ./cmd/histree-core` +- Test all: `make test` or `go test -v ./...` +- Test single file: `go test -v ./cmd/histree-core/main_test.go` +- Test specific test: `go test -v -run TestFormatVerboseWithTimezone ./cmd/histree-core` +- Clean: `make clean` + +## Code Style Guidelines +- Go version: 1.18+ +- Error handling: Use `fmt.Errorf("context: %w", err)` with error wrapping +- Function naming: Use camelCase +- Indentation: Tabs, not spaces +- Return early pattern for error handling +- Always close resources (db connections, file handles) with defer +- Use type definitions for constants (OutputFormat) +- Document exported functions and types +- SQL queries should be properly indented and use parametrized queries +- Time values should be stored in UTC, but displayed in local timezone \ No newline at end of file diff --git a/README.md b/README.md index 908dbb1..60ee291 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ This project was developed with the assistance of ChatGPT and GitHub Copilot. - **Directory-Aware History** Commands are stored with their execution directory context, allowing you to view history specific to directories. +- **Directory Path Updates** + When you move or rename directories, you can update all related history entries: + - Updates both exact path matches and subdirectory paths + - Preserves your command history context when reorganizing your filesystem + - Handles relative paths automatically + - **Shell Context Tracking** Each command is stored with its execution context: - Hostname of the machine @@ -59,13 +65,15 @@ import "github.com/fuba/histree-core/pkg/histree" ```sh -db string Path to SQLite database (required) --action string Action to perform: add or get +-action string Action to perform: add, get, or update-path -dir string Current directory for filtering entries -format string Output format: json, simple, or verbose (default "simple") -limit int Number of entries to retrieve (default 100) -hostname Hostname for command history (required for add action) --pid Process ID of the shell (required for add action) +-pid Process ID of the shell (required for add action) -exit int Exit code of the command +-old-path Old directory path (required for update-path action) +-new-path New directory path (required for update-path action) -v Show verbose output (same as -format verbose) ``` @@ -145,6 +153,16 @@ func main() { fmt.Fprintf(os.Stderr, "Failed to write entries: %v\n", err) os.Exit(1) } + + // Update directory paths (e.g., after moving directories) + oldPath := "/home/user/old-project-path" + newPath := "/home/user/new-project-path" + count, err := db.UpdatePaths(oldPath, newPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to update paths: %v\n", err) + os.Exit(1) + } + fmt.Printf("Updated %d history entries\n", count) } ``` @@ -182,6 +200,18 @@ $ histree -v # Different directory shows different history 2024-02-15T10:35:00 [/home/user/another-project] vim README.md 2024-02-15T10:35:30 [/home/user/another-project] git add README.md 2024-02-15T10:36:00 [/home/user/another-project] git commit -m "Update README" + +$ # Now let's move a directory and update history +$ mv ~/projects/web-app ~/projects/renamed-app +$ histree-core -db ~/.histree.db -action update-path -old-path ~/projects/web-app -new-path ~/projects/renamed-app +Updated 4 entries: /home/user/projects/web-app -> /home/user/projects/renamed-app + +$ cd ~/projects/renamed-app +$ histree -v # History is preserved with the new path +2024-02-15T10:30:15 [/home/user/projects/renamed-app] npm install +2024-02-15T10:31:20 [/home/user/projects/renamed-app] npm run build +2024-02-15T10:31:45 [/home/user/projects/renamed-app/dist] ls -la +2024-02-15T10:32:10 [/home/user/projects/renamed-app] git status ``` This example demonstrates how histree helps track your development workflow across different directories and projects, maintaining the context of your work. diff --git a/cmd/histree-core/main.go b/cmd/histree-core/main.go index 2152023..3328451 100644 --- a/cmd/histree-core/main.go +++ b/cmd/histree-core/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "time" @@ -15,7 +16,7 @@ import ( func main() { version := flag.Bool("version", false, "Show version information") dbPath := flag.String("db", "", "Path to SQLite database (required)") - action := flag.String("action", "", "Action to perform: add or get") + action := flag.String("action", "", "Action to perform: add, get, or update-path") limit := flag.Int("limit", 100, "Number of entries to retrieve") currentDir := flag.String("dir", "", "Current directory for filtering entries") format := flag.String("format", string(histree.FormatSimple), "Output format: json, simple, or verbose") @@ -23,6 +24,8 @@ func main() { processID := flag.Int("pid", 0, "Process ID (required for add action)") verbose := flag.Bool("v", false, "Show verbose output (same as -format verbose)") exitCode := flag.Int("exit", 0, "Exit code of the command") + oldPath := flag.String("old-path", "", "Old directory path (required for update-path action)") + newPath := flag.String("new-path", "", "New directory path (required for update-path action)") flag.Parse() if *version { @@ -70,6 +73,17 @@ func main() { fmt.Fprintf(os.Stderr, "Failed to get entries: %v\n", err) os.Exit(1) } + + case "update-path": + if *oldPath == "" || *newPath == "" { + fmt.Fprintf(os.Stderr, "Error: both -old-path and -new-path parameters are required for update-path action\n") + flag.Usage() + os.Exit(1) + } + if err := handleUpdatePath(db, *oldPath, *newPath); err != nil { + fmt.Fprintf(os.Stderr, "Failed to update paths: %v\n", err) + os.Exit(1) + } default: fmt.Fprintf(os.Stderr, "Unknown action: %s\n", *action) @@ -113,3 +127,35 @@ func handleGet(db *histree.DB, limit int, currentDir string, format histree.Outp return histree.WriteEntries(entries, os.Stdout, format) } + +func handleUpdatePath(db *histree.DB, oldPath, newPath string) error { + // Convert to absolute paths if they aren't already + if !filepath.IsAbs(oldPath) { + absOldPath, err := filepath.Abs(oldPath) + if err != nil { + return fmt.Errorf("failed to convert old path to absolute path: %w", err) + } + oldPath = absOldPath + } + + if !filepath.IsAbs(newPath) { + absNewPath, err := filepath.Abs(newPath) + if err != nil { + return fmt.Errorf("failed to convert new path to absolute path: %w", err) + } + newPath = absNewPath + } + + // Clean the paths to ensure consistent format + oldPath = filepath.Clean(oldPath) + newPath = filepath.Clean(newPath) + + // Update the paths in the database + count, err := db.UpdatePaths(oldPath, newPath) + if err != nil { + return err + } + + fmt.Printf("Updated %d entries: %s -> %s\n", count, oldPath, newPath) + return nil +} diff --git a/cmd/histree-core/main_test.go b/cmd/histree-core/main_test.go index 819a0f1..3069711 100644 --- a/cmd/histree-core/main_test.go +++ b/cmd/histree-core/main_test.go @@ -208,3 +208,92 @@ func TestFormatVerboseWithSpecificTimezones(t *testing.T) { }) } } + +// TestUpdatePaths tests the UpdatePaths functionality +func TestUpdatePaths(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + + // Define test paths + oldPath := "/home/user/oldpath" + newPath := "/home/user/newpath" + + // Create test entries with different paths + entries := []histree.HistoryEntry{ + { + Command: "cd /home/user/oldpath", + Directory: oldPath, + Timestamp: time.Now().UTC(), + ExitCode: 0, + Hostname: "test-host", + ProcessID: 12345, + }, + { + Command: "ls -la", + Directory: oldPath + "/subdir", + Timestamp: time.Now().UTC(), + ExitCode: 0, + Hostname: "test-host", + ProcessID: 12345, + }, + { + Command: "echo 'unrelated'", + Directory: "/tmp", + Timestamp: time.Now().UTC(), + ExitCode: 0, + Hostname: "test-host", + ProcessID: 12345, + }, + } + + // Add test entries + for _, entry := range entries { + if err := db.AddEntry(&entry); err != nil { + t.Fatalf("Failed to add test entry: %v", err) + } + } + + // Update paths + count, err := db.UpdatePaths(oldPath, newPath) + if err != nil { + t.Fatalf("Failed to update paths: %v", err) + } + + // Should have updated 2 entries (main path and subdirectory) + if count != 2 { + t.Errorf("Expected 2 entries to be updated, got %d", count) + } + + // Verify the updates + rows, err := db.Query("SELECT directory FROM history ORDER BY id") + if err != nil { + t.Fatalf("Failed to query entries: %v", err) + } + defer rows.Close() + + var updatedDirs []string + for rows.Next() { + var dir string + if err := rows.Scan(&dir); err != nil { + t.Fatalf("Failed to scan row: %v", err) + } + updatedDirs = append(updatedDirs, dir) + } + + // Check the expected path changes + expectedDirs := []string{ + newPath, // oldPath should now be newPath + newPath + "/subdir", // oldPath/subdir should now be newPath/subdir + "/tmp", // Unrelated path should remain unchanged + } + + if len(updatedDirs) != len(expectedDirs) { + t.Fatalf("Expected %d entries, got %d", len(expectedDirs), len(updatedDirs)) + } + + for i, expected := range expectedDirs { + if updatedDirs[i] != expected { + t.Errorf("Entry %d: expected directory %q, got %q", i, expected, updatedDirs[i]) + } + } +} diff --git a/pkg/histree/histree.go b/pkg/histree/histree.go index b73ae98..6ab05c9 100644 --- a/pkg/histree/histree.go +++ b/pkg/histree/histree.go @@ -11,7 +11,7 @@ import ( // Version information const ( - Version = "v0.3.4" + Version = "v0.3.5" ) // OutputFormat defines how history entries are formatted when displayed @@ -152,6 +152,42 @@ func (db *DB) AddEntry(entry *HistoryEntry) error { return nil } +// UpdatePaths updates directory paths in history entries from oldPath to newPath +func (db *DB) UpdatePaths(oldPath, newPath string) (int64, error) { + tx, err := db.Begin() + if err != nil { + return 0, fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + // Update entries where directory exactly matches oldPath + exactResult, err := tx.Exec( + "UPDATE history SET directory = ? WHERE directory = ?", + newPath, oldPath, + ) + if err != nil { + return 0, fmt.Errorf("failed to update exact matches: %w", err) + } + + // Update entries where directory is a subdirectory of oldPath + subResult, err := tx.Exec( + "UPDATE history SET directory = REPLACE(directory, ?, ?) WHERE directory LIKE ? || '/%'", + oldPath, newPath, oldPath, + ) + if err != nil { + return 0, fmt.Errorf("failed to update subdirectory matches: %w", err) + } + + if err := tx.Commit(); err != nil { + return 0, fmt.Errorf("failed to commit transaction: %w", err) + } + + // Get total number of rows affected by both updates + exactCount, _ := exactResult.RowsAffected() + subCount, _ := subResult.RowsAffected() + return exactCount + subCount, nil +} + // GetEntries retrieves command history entries from the database func (db *DB) GetEntries(limit int, currentDir string) ([]HistoryEntry, error) { entries := make([]HistoryEntry, 0, limit)