Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
```

Expand Down Expand Up @@ -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)
}
```

Expand Down Expand Up @@ -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.
Expand Down
48 changes: 47 additions & 1 deletion cmd/histree-core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"

Expand All @@ -15,14 +16,16 @@ 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")
hostname := flag.String("hostname", "", "Hostname (required for add action)")
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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
89 changes: 89 additions & 0 deletions cmd/histree-core/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}
}
38 changes: 37 additions & 1 deletion pkg/histree/histree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down