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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Fixed

- Calendar: report multi-calendar event truncation on stderr for text output and as per-calendar page tokens in JSON. (#831) — thanks @TurboTheTurtle.
- Docs: update the Docker authentication example to persist file-keyring tokens with `GOG_HOME`. (#828, #830) — thanks @WadydX.

## 0.28.0 - 2026-06-15
Expand Down
13 changes: 11 additions & 2 deletions internal/cmd/calendar_all_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) {
"attendees": []map[string]any{{"email": "a@example.com"}},
},
},
"nextPageToken": "cal1-next",
})
return
case strings.Contains(r.URL.Path, "/calendars/cal2/events") && r.Method == http.MethodGet:
Expand Down Expand Up @@ -68,14 +69,18 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) {
})

var parsed struct {
Events []map[string]any `json:"events"`
Events []map[string]any `json:"events"`
NextPageTokens []calendarEventsNextPage `json:"nextPageTokens"`
}
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
t.Fatalf("json parse: %v", err)
}
if len(parsed.Events) != 2 {
t.Fatalf("unexpected events: %#v", parsed.Events)
}
if len(parsed.NextPageTokens) != 1 || parsed.NextPageTokens[0].CalendarID != "cal1" || parsed.NextPageTokens[0].NextPageToken != "cal1-next" {
t.Fatalf("unexpected nextPageTokens: %#v", parsed.NextPageTokens)
}
}

// TestListAllCalendarsEvents_SortByStart verifies that --sort=start orders
Expand Down Expand Up @@ -127,14 +132,18 @@ func TestListAllCalendarsEvents_SortByStart(t *testing.T) {
})

var parsed struct {
Events []map[string]any `json:"events"`
Events []map[string]any `json:"events"`
NextPageTokens []calendarEventsNextPage `json:"nextPageTokens"`
}
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
t.Fatalf("json parse: %v", err)
}
if len(parsed.Events) != 2 {
t.Fatalf("expected 2 events, got %#v", parsed.Events)
}
if len(parsed.NextPageTokens) != 0 {
t.Fatalf("unexpected nextPageTokens: %#v", parsed.NextPageTokens)
}
if got, _ := parsed.Events[0]["id"].(string); got != "early" {
t.Fatalf("expected first event id 'early', got %q (events: %#v)", got, parsed.Events)
}
Expand Down
40 changes: 37 additions & 3 deletions internal/cmd/calendar_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func listSelectedCalendarsEvents(ctx context.Context, svc *calendar.Service, cal
func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarIDs []string, from, to string, maxResults int64, page string, allPages bool, failEmpty bool, query, privatePropFilter, sharedPropFilter, fields string, showWeekday bool, showLocation bool, timezoneHints map[string]calendarTimezoneHint, sortKey, sortOrder string) error {
u := ui.FromContext(ctx)
all := []*eventWithCalendar{}
nextPages := []calendarEventsNextPage{}
for _, calID := range calendarIDs {
calID = strings.TrimSpace(calID)
if calID == "" {
Expand All @@ -159,11 +160,17 @@ func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarI
return resp.Items, resp.NextPageToken, nil
}

events, _, err := loadPagedItems(page, allPages, fetch)
events, nextPageToken, err := loadPagedItems(page, allPages, fetch)
if err != nil {
u.Err().Linef("calendar %s: %v", calID, err)
continue
}
if nextPageToken != "" {
nextPages = append(nextPages, calendarEventsNextPage{
CalendarID: calID,
NextPageToken: nextPageToken,
})
}

for _, e := range events {
redactCalendarEventForOutput(ctx, e)
Expand All @@ -174,15 +181,42 @@ func listCalendarIDsEvents(ctx context.Context, svc *calendar.Service, calendarI
sortEventsBy(all, sortKey, sortOrder)

if outfmt.IsJSON(ctx) {
if err := outfmt.WriteJSON(ctx, stdoutWriter(ctx), map[string]any{"events": all}); err != nil {
if err := outfmt.WriteJSON(ctx, stdoutWriter(ctx), map[string]any{
"events": all,
"nextPageTokens": nextPages,
}); err != nil {
return err
}
if len(all) == 0 {
return failEmptyExit(failEmpty)
}
return nil
}
return renderCalendarEventsTable(ctx, all, "", true, showWeekday, showLocation, failEmpty, false)
if err := renderCalendarEventsTable(ctx, all, "", true, showWeekday, showLocation, failEmpty, false); err != nil {
return err
}
printCalendarEventsNextPageHint(u, len(calendarIDs), nextPages)
return nil
}

type calendarEventsNextPage struct {
CalendarID string `json:"calendarId"`
NextPageToken string `json:"nextPageToken"`
}

func printCalendarEventsNextPageHint(u *ui.UI, calendarCount int, nextPages []calendarEventsNextPage) {
if u == nil || len(nextPages) == 0 {
return
}
if calendarCount == 1 {
printNextPageHintWithAll(u, nextPages[0].NextPageToken, "--all-pages")
return
}
if len(nextPages) == 1 {
u.Err().Linef("# More results: use --all-pages to fetch every page (%s has more results)", nextPages[0].CalendarID)
return
}
u.Err().Linef("# More results: use --all-pages to fetch every page (%d calendars have more results)", len(nextPages))
}

func renderCalendarEventsTable(ctx context.Context, events []*eventWithCalendar, nextPageToken string, includeCalendar, showWeekday, showLocation, failEmpty bool, printPageHint bool) error {
Expand Down
57 changes: 57 additions & 0 deletions internal/cmd/execute_calendar_events_text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,60 @@ func TestExecute_CalendarEvents_Text_All(t *testing.T) {
t.Fatalf("unexpected out=%q", out)
}
}

func TestExecute_CalendarEvents_Text_AllWithPagingHint(t *testing.T) {
srv := httptest.NewServer(withPrimaryCalendar(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "/users/me/calendarList"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"items": []map[string]any{
{"id": "c1"},
{"id": "c2"},
},
})
return
case strings.Contains(r.URL.Path, "/calendars/c1/events"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"items": []map[string]any{
{"id": "e1", "summary": "S1", "start": map[string]any{"dateTime": "2025-12-17T10:00:00Z"}, "end": map[string]any{"dateTime": "2025-12-17T11:00:00Z"}},
},
"nextPageToken": "c1-next",
})
return
case strings.Contains(r.URL.Path, "/calendars/c2/events"):
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"items": []map[string]any{
{"id": "e2", "summary": "S2", "start": map[string]any{"dateTime": "2025-12-17T12:00:00Z"}, "end": map[string]any{"dateTime": "2025-12-17T13:00:00Z"}},
},
})
return
default:
http.NotFound(w, r)
return
}
})))
defer srv.Close()

svc, err := calendar.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
result := executeWithCalendarTestService(t, []string{"--account", "a@b.com", "calendar", "events", "--all", "--from", "2025-12-17T00:00:00Z", "--to", "2025-12-18T00:00:00Z"}, svc)
if result.err != nil {
t.Fatalf("Execute: %v", result.err)
}
if !strings.Contains(result.stderr, "# More results: use --all-pages to fetch every page (c1 has more results)") {
t.Fatalf("unexpected stderr=%q", result.stderr)
}
out := result.stdout
if !strings.Contains(out, "CALENDAR") || !strings.Contains(out, "c1") || !strings.Contains(out, "e1") || !strings.Contains(out, "S1") || !strings.Contains(out, "c2") || !strings.Contains(out, "e2") || !strings.Contains(out, "S2") {
t.Fatalf("unexpected out=%q", out)
}
}
Loading