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
44 changes: 24 additions & 20 deletions commands/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log/slog"
"os"
"strconv"
"strings"
"time"

"github.com/gookit/color"
Expand Down Expand Up @@ -93,14 +94,14 @@ func commandGrep(c *cli.Context) error {
return fmt.Errorf("unsupported format: %s. Use 'table' or 'json'", format)
}

// Get search text from args
searchText := c.Args().First()
// Get search text from args; unquoted words form one phrase (rg git sub == rg "git sub")
searchText := strings.TrimSpace(strings.Join(c.Args().Slice(), " "))
slog.Debug("grep command args",
slog.String("first", searchText),
slog.String("searchText", searchText),
slog.Int("nArgs", c.NArg()),
slog.Any("allArgs", c.Args().Slice()))
if searchText == "" {
return fmt.Errorf("search text is required. Usage: shelltime grep <search-text>")
return fmt.Errorf("search text is required. Usage: shelltime rg <search-text>")
}

// Read config to get endpoint and token
Expand Down Expand Up @@ -209,24 +210,27 @@ func buildGrepFilter(c *cli.Context, searchText string) (*model.SearchCommandsFi
filter.MainCommand = []string{mainCmd}
}

// Handle time filters with flexible date parsing
var timeFilters []float64
if since := c.String("since"); since != "" {
t, err := parseFlexibleDate(since, false)
if err != nil {
return nil, fmt.Errorf("invalid --since date: %w", err)
// Handle time filters with flexible date parsing. The server expects a
// [start, end] pair in unix seconds; a single value means "that one day",
// so an open side is filled in (epoch for start, now for end).
since, until := c.String("since"), c.String("until")
if since != "" || until != "" {
start, end := time.Unix(0, 0), time.Now()
if since != "" {
t, err := parseFlexibleDate(since, false)
if err != nil {
return nil, fmt.Errorf("invalid --since date: %w", err)
}
start = t
}
timeFilters = append(timeFilters, float64(t.UnixMilli()))
}
if until := c.String("until"); until != "" {
t, err := parseFlexibleDate(until, true)
if err != nil {
return nil, fmt.Errorf("invalid --until date: %w", err)
if until != "" {
t, err := parseFlexibleDate(until, true)
if err != nil {
return nil, fmt.Errorf("invalid --until date: %w", err)
}
end = t
}
timeFilters = append(timeFilters, float64(t.UnixMilli()))
}
if len(timeFilters) > 0 {
filter.Time = timeFilters
filter.Time = []float64{float64(start.Unix()), float64(end.Unix())}
}

return filter, nil
Expand Down
60 changes: 58 additions & 2 deletions commands/grep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,40 @@ func TestBuildGrepFilter_SinceAndUntilTimeWindow(t *testing.T) {
filter, err := buildGrepFilter(c, "x")
require.NoError(t, err)

// The server reads the window in unix seconds.
require.Len(t, filter.Time, 2)
since := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
assert.Equal(t, float64(since.UnixMilli()), filter.Time[0])
assert.Equal(t, float64(since.Unix()), filter.Time[0])
// until=2024-06 end-of-period -> 2024-06-30 23:59:59 UTC.
untilEnd := time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC)
assert.Equal(t, float64(untilEnd.UnixMilli()), filter.Time[1])
assert.Equal(t, float64(untilEnd.Unix()), filter.Time[1])
}

func TestBuildGrepFilter_SinceOnlyRunsUntilNow(t *testing.T) {
c := newGrepContext(t, func(fs *flag.FlagSet) {
require.NoError(t, fs.Set("since", "2024-01-15"))
})
filter, err := buildGrepFilter(c, "x")
require.NoError(t, err)

// A single value would be read as "that one day", so the end is filled in.
require.Len(t, filter.Time, 2)
since := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
assert.Equal(t, float64(since.Unix()), filter.Time[0])
assert.InDelta(t, float64(time.Now().Unix()), filter.Time[1], 5)
}

func TestBuildGrepFilter_UntilOnlyStartsAtEpoch(t *testing.T) {
c := newGrepContext(t, func(fs *flag.FlagSet) {
require.NoError(t, fs.Set("until", "2024-01-15"))
})
filter, err := buildGrepFilter(c, "x")
require.NoError(t, err)

require.Len(t, filter.Time, 2)
assert.Equal(t, float64(0), filter.Time[0])
untilEnd := time.Date(2024, 1, 15, 23, 59, 59, 0, time.UTC)
assert.Equal(t, float64(untilEnd.Unix()), filter.Time[1])
}

func TestBuildGrepFilter_InvalidSince(t *testing.T) {
Expand Down Expand Up @@ -327,6 +355,34 @@ func TestCommandGrep_SuccessJSON(t *testing.T) {
assert.True(t, strings.Contains(gotBody, "fetchCommands"), "request body should contain the query")
}

func TestCommandGrep_UnquotedWordsFormOnePhrase(t *testing.T) {
mc := setupGrepActionTest(t)

var gotFilter model.SearchCommandsFilter
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Variables struct {
Filter model.SearchCommandsFilter `json:"filter"`
} `json:"variables"`
}
assert.NoError(t, json.NewDecoder(r.Body).Decode(&body))
gotFilter = body.Variables.Filter
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"data":{"fetchCommands":{"count":0,"edges":[]}}}`)
}))
t.Cleanup(server.Close)

mc.On("ReadConfigFile", mock.Anything).Return(model.ShellTimeConfig{
Token: "tok",
APIEndpoint: server.URL,
}, nil)

app := &cli.App{Name: "t", Commands: []*cli.Command{GrepCommand}}
// `shelltime rg git sub` must search "git sub", not just "git".
require.NoError(t, app.Run([]string{"t", "rg", "git", "sub"}))
assert.Equal(t, "git sub", gotFilter.Command)
}

func TestCommandGrep_NoResults(t *testing.T) {
mc := setupGrepActionTest(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
Loading