Skip to content

Commit d5b9bf3

Browse files
committed
feat(query): add --from/--to/--range scan-bounding for large repos
Bounds the git log scan so huge histories (e.g. Linux kernel, ~1.5M commits) aren't walked in full. --from/--to accept any git date; --range takes a ref range (e.g. main...feature) which also serves per-PR stats. Flags work before or after the positional arg. LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%)
1 parent a92494d commit d5b9bf3

5 files changed

Lines changed: 109 additions & 25 deletions

File tree

cmd/app.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,11 +390,18 @@ EXAMPLES
390390
391391
# Save and reuse your own query
392392
lrc query --add "SELECT date, subject FROM review_log WHERE action='skipped'" --name skipped
393-
lrc query skipped --json`,
393+
lrc query skipped --json
394+
395+
# Bound the scan on huge repos (Linux kernel = ~1.5M commits)
396+
lrc query stats --from "2024-01-01" --to "2024-12-31"
397+
lrc query stats --range main...feature # just this PR's commits`,
394398
Flags: []cli.Flag{
395399
&cli.BoolFlag{Name: "json", Usage: "output machine-readable JSON"},
396400
&cli.StringFlag{Name: "add", Usage: "save the given SQL as an alias (requires --name)"},
397401
&cli.StringFlag{Name: "name", Usage: "alias name to save with --add"},
402+
&cli.StringFlag{Name: "from", Usage: "only scan commits since this git date (e.g. 2024-01-01, '2 weeks ago') — bounds large repos"},
403+
&cli.StringFlag{Name: "to", Usage: "only scan commits until this git date"},
404+
&cli.StringFlag{Name: "range", Usage: "only scan a ref range, e.g. main...feature (per-PR stats)"},
398405
},
399406
Action: h.RunQuery,
400407
Subcommands: []*cli.Command{

internal/reviewquery/command.go

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,13 @@ func RunQuery(c *cli.Context) error {
2222
return nil
2323
}
2424

25-
// urfave/cli stops parsing flags at the first positional arg, so support a
26-
// trailing --json too (e.g. `lrc query stats --json`).
25+
// Seed from flags placed BEFORE the positional arg (cli parses those).
2726
jsonOut := c.Bool("json")
28-
positionals := make([]string, 0, c.NArg())
29-
for _, a := range c.Args().Slice() {
30-
switch a {
31-
case "--json", "-json", "-j":
32-
jsonOut = true
33-
default:
34-
positionals = append(positionals, a)
35-
}
36-
}
27+
filter := Filter{From: c.String("from"), To: c.String("to"), Range: c.String("range")}
28+
29+
// urfave/cli stops parsing flags at the first positional arg, so also scan
30+
// the remaining args for trailing flags (e.g. `lrc query stats --from 2024-01-01`).
31+
positionals := parseTrailingFlags(c.Args().Slice(), &jsonOut, &filter)
3732

3833
arg := "stats" // default alias
3934
if len(positionals) > 0 && strings.TrimSpace(positionals[0]) != "" {
@@ -49,7 +44,7 @@ func RunQuery(c *cli.Context) error {
4944
sqlText = strings.Join(positionals, " ")
5045
}
5146

52-
res, err := Run(Filter{}, sqlText)
47+
res, err := Run(filter, sqlText)
5348
if err != nil {
5449
return err
5550
}
@@ -66,6 +61,50 @@ func RunQuery(c *cli.Context) error {
6661
return nil
6762
}
6863

64+
// parseTrailingFlags pulls flags out of args that cli left unparsed (anything
65+
// after the first positional). Supports `--flag value` and `--flag=value`.
66+
// Returns the remaining positional args; sets jsonOut/filter via pointers.
67+
func parseTrailingFlags(args []string, jsonOut *bool, filter *Filter) []string {
68+
positionals := make([]string, 0, len(args))
69+
for i := 0; i < len(args); i++ {
70+
a := args[i]
71+
// flag=value form
72+
switch {
73+
case a == "--json" || a == "-j":
74+
*jsonOut = true
75+
continue
76+
case strings.HasPrefix(a, "--from="):
77+
filter.From = strings.TrimPrefix(a, "--from=")
78+
continue
79+
case strings.HasPrefix(a, "--to="):
80+
filter.To = strings.TrimPrefix(a, "--to=")
81+
continue
82+
case strings.HasPrefix(a, "--range="):
83+
filter.Range = strings.TrimPrefix(a, "--range=")
84+
continue
85+
}
86+
// flag value form (consume next arg)
87+
if i+1 < len(args) {
88+
switch a {
89+
case "--from":
90+
filter.From = args[i+1]
91+
i++
92+
continue
93+
case "--to":
94+
filter.To = args[i+1]
95+
i++
96+
continue
97+
case "--range":
98+
filter.Range = args[i+1]
99+
i++
100+
continue
101+
}
102+
}
103+
positionals = append(positionals, a)
104+
}
105+
return positionals
106+
}
107+
69108
// RunQueryList prints every alias and its source.
70109
func RunQueryList(c *cli.Context) error {
71110
aliases, err := ListAliases()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package reviewquery
2+
3+
import "testing"
4+
5+
func TestParseTrailingFlags(t *testing.T) {
6+
cases := []struct {
7+
name string
8+
args []string
9+
wantPos []string
10+
from string
11+
to string
12+
rng string
13+
json bool
14+
}{
15+
{"none", []string{"stats"}, []string{"stats"}, "", "", "", false},
16+
{"trailing json", []string{"stats", "--json"}, []string{"stats"}, "", "", "", true},
17+
{"from value", []string{"stats", "--from", "2024-01-01"}, []string{"stats"}, "2024-01-01", "", "", false},
18+
{"from equals", []string{"stats", "--from=2024-01-01"}, []string{"stats"}, "2024-01-01", "", "", false},
19+
{"range+json", []string{"q", "--range", "main...dev", "--json"}, []string{"q"}, "", "", "main...dev", true},
20+
{"to", []string{"stats", "--to=2025-12-31"}, []string{"stats"}, "", "2025-12-31", "", false},
21+
}
22+
for _, tc := range cases {
23+
t.Run(tc.name, func(t *testing.T) {
24+
jsonOut := false
25+
f := Filter{}
26+
pos := parseTrailingFlags(tc.args, &jsonOut, &f)
27+
if len(pos) != len(tc.wantPos) || (len(pos) > 0 && pos[0] != tc.wantPos[0]) {
28+
t.Errorf("positionals = %v; want %v", pos, tc.wantPos)
29+
}
30+
if f.From != tc.from || f.To != tc.to || f.Range != tc.rng || jsonOut != tc.json {
31+
t.Errorf("got from=%q to=%q range=%q json=%v; want from=%q to=%q range=%q json=%v",
32+
f.From, f.To, f.Range, jsonOut, tc.from, tc.to, tc.rng, tc.json)
33+
}
34+
})
35+
}
36+
}

internal/reviewquery/extract.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ func Extract(f Filter) ([]ReviewRecord, error) {
9797
if f.Author != "" {
9898
args = append(args, "--author="+f.Author)
9999
}
100-
if !f.Since.IsZero() {
101-
args = append(args, "--since="+f.Since.Format(time.RFC3339))
100+
if f.From != "" {
101+
args = append(args, "--since="+f.From)
102102
}
103-
if !f.Until.IsZero() {
104-
args = append(args, "--until="+f.Until.Format(time.RFC3339))
103+
if f.To != "" {
104+
args = append(args, "--until="+f.To)
105105
}
106106
if f.Range != "" {
107107
args = append(args, f.Range)

internal/reviewquery/model.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ type ReviewRecord struct {
2121
CoveragePct int // coverage percent (0 if absent)
2222
}
2323

24-
// Filter narrows which commits are extracted. Phase 1 uses Range/Since; the
25-
// rest are wired in Phase 2.
24+
// Filter narrows (and bounds) which commits are scanned. From/To/Range bound the
25+
// git log so huge repos (e.g. the Linux kernel, ~1.5M commits) don't get walked
26+
// in full. From/To are passed straight to git, so they accept any git date
27+
// (e.g. "2024-01-01", "2 weeks ago").
2628
type Filter struct {
27-
Range string // e.g. "main...feature" (PR diff); empty = full history
28-
Since time.Time // zero = no lower bound
29-
Until time.Time // zero = no upper bound
30-
Author string // substring match on author/email
31-
PathPrefix string // limit to commits touching this path
32-
Action string // limit to one action
29+
Range string // e.g. "main...feature" (PR diff); empty = full history
30+
From string // git --since bound (lower); empty = no lower bound
31+
To string // git --until bound (upper); empty = no upper bound
32+
Author string // substring match on author/email
33+
PathPrefix string // limit to commits touching this path
34+
Action string // limit to one action
3335
}
3436

3537
// Alias is a saved, named SQL query stored in ~/.lrc/queries.toml.

0 commit comments

Comments
 (0)