From 2edfeebefc9f29058dd79002eb4b35b7c664fc9f Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:22:06 -0700 Subject: [PATCH 1/2] Allow report show to use short IDs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e02217cd-ffed-496e-b8ea-848a781cda64 --- cmd/report.go | 36 ++++++++++++++++++++++++++++++++++-- cmd/report_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/cmd/report.go b/cmd/report.go index 73114e3b..b268ea40 100644 --- a/cmd/report.go +++ b/cmd/report.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/url" + "strings" "time" "github.com/jongio/grut/internal/crashlog" @@ -140,9 +141,13 @@ func writeCrashReportListJSON(w io.Writer, reports []*crashlog.CrashReport) erro } func runReportShow(id string) error { - report, err := crashlog.Read(id) + reports, err := crashlog.List() + if err != nil { + return fmt.Errorf("listing crash reports: %w", err) + } + report, err := findCrashReport(id, reports) if err != nil { - return fmt.Errorf("reading crash report %q: %w", id, err) + return err } data, err := json.MarshalIndent(report, "", " ") @@ -153,6 +158,33 @@ func runReportShow(id string) error { return nil } +func findCrashReport(id string, reports []*crashlog.CrashReport) (*crashlog.CrashReport, error) { + id = strings.TrimSpace(id) + if id == "" { + return nil, fmt.Errorf("crash report ID is required") + } + for _, report := range reports { + if report.ID == id { + return report, nil + } + } + + var matches []*crashlog.CrashReport + for _, report := range reports { + if strings.HasPrefix(report.ID, id) { + matches = append(matches, report) + } + } + switch len(matches) { + case 1: + return matches[0], nil + case 0: + return nil, fmt.Errorf("crash report with id %q not found", id) + default: + return nil, fmt.Errorf("crash report id %q matches %d reports; use a longer ID", id, len(matches)) + } +} + func runReportLatest(noBrowser bool) error { reports, err := crashlog.List() if err != nil { diff --git a/cmd/report_test.go b/cmd/report_test.go index 5e46b41b..5b4b3979 100644 --- a/cmd/report_test.go +++ b/cmd/report_test.go @@ -196,6 +196,51 @@ func TestRunReport_ShowFlag_NonexistentID(t *testing.T) { assert.Error(t, err) } +func TestFindCrashReport_ExactID(t *testing.T) { + reports := []*crashlog.CrashReport{ + {ID: "123456789"}, + {ID: "123400000"}, + } + + report, err := findCrashReport("123456789", reports) + + require.NoError(t, err) + assert.Equal(t, "123456789", report.ID) +} + +func TestFindCrashReport_UnambiguousPrefix(t *testing.T) { + reports := []*crashlog.CrashReport{ + {ID: "123456789"}, + {ID: "987654321"}, + } + + report, err := findCrashReport("1234", reports) + + require.NoError(t, err) + assert.Equal(t, "123456789", report.ID) +} + +func TestFindCrashReport_AmbiguousPrefix(t *testing.T) { + reports := []*crashlog.CrashReport{ + {ID: "123456789"}, + {ID: "123400000"}, + } + + _, err := findCrashReport("1234", reports) + + require.Error(t, err) + assert.Contains(t, err.Error(), "matches 2 reports") +} + +func TestFindCrashReport_MissingPrefix(t *testing.T) { + reports := []*crashlog.CrashReport{{ID: "123456789"}} + + _, err := findCrashReport("999", reports) + + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") +} + func TestRunReport_JSONWithoutListReturnsError(t *testing.T) { cmd := newReportCmd() cmd.SetArgs([]string{"--json"}) From f159f192a11de9c86bd0c4cb7aceadf4e55b623d Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:20:39 -0700 Subject: [PATCH 2/2] fix: remove unreachable crashlog.Read after short-ID lookup runReportShow now resolves reports through List plus findCrashReport, so Read had no remaining callers and the deadcode gate failed on it. Removes the function and its two orphaned tests, and reworks TestWriteAndRead to verify the written report through List. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 66a101e5-8e3c-41f0-bcf4-e26e7a44f1ed --- internal/crashlog/crashlog_test.go | 30 ++++-------------------- internal/crashlog/reader.go | 37 ------------------------------ 2 files changed, 5 insertions(+), 62 deletions(-) diff --git a/internal/crashlog/crashlog_test.go b/internal/crashlog/crashlog_test.go index c6046a6c..8745ad4a 100644 --- a/internal/crashlog/crashlog_test.go +++ b/internal/crashlog/crashlog_test.go @@ -136,7 +136,7 @@ func TestFormatGitHubIssueBody(t *testing.T) { } // --------------------------------------------------------------------------- -// Write / Read / List / Clear / PruneOld +// Write / List / Clear / PruneOld // --------------------------------------------------------------------------- func TestWriteAndRead(t *testing.T) { @@ -147,10 +147,11 @@ func TestWriteAndRead(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, path) - got, err := Read(r.ID) + reports, err := List() require.NoError(t, err) - assert.Equal(t, r.PanicValue, got.PanicValue) - assert.Equal(t, r.ID, got.ID) + require.Len(t, reports, 1) + assert.Equal(t, r.PanicValue, reports[0].PanicValue) + assert.Equal(t, r.ID, reports[0].ID) } func TestWriteAndList(t *testing.T) { @@ -602,27 +603,6 @@ func TestList_MalformedJSON(t *testing.T) { assert.Equal(t, "valid", reports[0].PanicValue) } -func TestRead_NotFound(t *testing.T) { - setTestDataHome(t) - - // Write a report so the directory exists. - r := NewReport("exists", []byte("stack"), "ctx") - _, err := Write(r) - require.NoError(t, err) - - _, err = Read("nonexistent-id") - assert.Error(t, err) - assert.Contains(t, err.Error(), "not found") -} - -func TestRead_NoDirectory(t *testing.T) { - setTestDataHome(t) - - _, err := Read("any-id") - assert.Error(t, err) - assert.Contains(t, err.Error(), "no crash reports found") -} - func TestClear_EmptyDirectory(t *testing.T) { setTestDataHome(t) diff --git a/internal/crashlog/reader.go b/internal/crashlog/reader.go index 0515dbbb..89c3c434 100644 --- a/internal/crashlog/reader.go +++ b/internal/crashlog/reader.go @@ -54,43 +54,6 @@ func List() ([]*CrashReport, error) { return reports, nil } -// Read loads a single crash report by its ID. It returns an error if no -// report with the given ID exists. -func Read(id string) (*CrashReport, error) { - if len(id) > 64 { - return nil, fmt.Errorf("invalid crash report ID") - } - dir := crashDir() - entries, err := os.ReadDir(dir) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil, fmt.Errorf("no crash reports found") - } - return nil, fmt.Errorf("reading crash directory: %w", err) - } - - for _, e := range entries { - if e.IsDir() || !strings.HasPrefix(e.Name(), "crash-") || !strings.HasSuffix(e.Name(), ".json") { - continue - } - data, err := os.ReadFile(filepath.Join(dir, e.Name())) - if err != nil { - slog.Warn("skipping unreadable crash report", "file", e.Name(), "error", err) - continue - } - var r CrashReport - if err := json.Unmarshal(data, &r); err != nil { - slog.Warn("skipping malformed crash report", "file", e.Name(), "error", err) - continue - } - if r.ID == id { - return &r, nil - } - } - - return nil, fmt.Errorf("crash report with id %q not found", id) -} - // Clear deletes all crash report files from the crash directory and // returns the number of files removed. func Clear() (int, error) {