Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6e416bd

Browse files
feat(calendar): add event sorting
Add calendar events --sort/--order support and regenerate command docs.\n\nCo-authored-by: gado-ships-it <[email protected]>\nCo-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 0b0b338 commit 6e416bd

9 files changed

Lines changed: 244 additions & 40 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66

7+
- Calendar: add `calendar events --sort=start|end|summary|calendar` and `--order=asc|desc` so `--all` output can be returned chronologically across calendars instead of per-calendar API iteration order. Also documents `now` in the `--from`/`--to` help strings (already accepted by `timeparse`) — the relative form agents need when planning "from now on" — thanks @gado-ships-it.
78
- Drive: add `drive share --notify` for invite targets that require a Drive notification email.
89
- Calendar: keep `calendar appointments` as an explicit diagnostic because the Calendar API still rejects `eventTypes=appointmentSchedule`. (#329)
910
- CLI: add nested `docs tabs ...` and `forms questions ...` aliases for consistent sub-item command patterns while preserving existing flat commands. (#433)

docs/commands/gog-calendar-events.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,22 @@ gog calendar (cal) events (list,ls) [<calendarId> ...] [flags]
3333
| `--fail-empty`<br>`--non-empty`<br>`--require-results` | `bool` | | Exit with code 3 if no results |
3434
| `--fields` | `string` | | Comma-separated fields to return |
3535
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
36-
| `--from` | `string` | | Start time (RFC3339 with timezone, date, or relative: today, tomorrow, monday) |
36+
| `--from` | `string` | | Start time (RFC3339 with timezone, date, or relative: now, today, tomorrow, monday) |
3737
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
3838
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
3939
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
4040
| `--max`<br>`--limit` | `int64` | 10 | Max results |
4141
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
42+
| `--order` | `string` | asc | Sort order |
4243
| `--page`<br>`--cursor` | `string` | | Page token |
4344
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
4445
| `--private-prop-filter` | `string` | | Filter by private extended property (key=value) |
4546
| `--query` | `string` | | Free text search |
4647
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
4748
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
4849
| `--shared-prop-filter` | `string` | | Filter by shared extended property (key=value) |
49-
| `--to` | `string` | | End time (RFC3339 with timezone, date, or relative) |
50+
| `--sort` | `string` | | Sort events by start\|end\|summary\|calendar (default: keep API order; with --all, start is recommended for chronological output) |
51+
| `--to` | `string` | | End time (RFC3339 with timezone, date, or relative: now, today, tomorrow, monday) |
5052
| `--today` | `bool` | | Today only (timezone-aware) |
5153
| `--tomorrow` | `bool` | | Tomorrow only (timezone-aware) |
5254
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |

docs/commands/gog-calendar-search.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ gog calendar (cal) search (find,query) <query> [flags]
2828
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
2929
| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
3030
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
31-
| `--from` | `string` | | Start time (RFC3339, date, or relative: today, tomorrow, monday) |
31+
| `--from` | `string` | | Start time (RFC3339, date, or relative: now, today, tomorrow, monday) |
3232
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
3333
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
3434
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
@@ -37,7 +37,7 @@ gog calendar (cal) search (find,query) <query> [flags]
3737
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
3838
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
3939
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
40-
| `--to` | `string` | | End time (RFC3339, date, or relative) |
40+
| `--to` | `string` | | End time (RFC3339, date, or relative: now, today, tomorrow, monday) |
4141
| `--today` | `bool` | | Today only |
4242
| `--tomorrow` | `bool` | | Tomorrow only |
4343
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |

docs/commands/gog-calendar-team.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ gog calendar (cal) team <group-email> [flags]
2828
| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
2929
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
3030
| `--freebusy` | `bool` | | Show only busy/free blocks (faster, single API call) |
31-
| `--from` | `string` | | Start time (RFC3339, date, or relative: today, tomorrow, monday) |
31+
| `--from` | `string` | | Start time (RFC3339, date, or relative: now, today, tomorrow, monday) |
3232
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
3333
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
3434
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
@@ -39,7 +39,7 @@ gog calendar (cal) team <group-email> [flags]
3939
| `-q`<br>`--query` | `string` | | Filter events by title (case-insensitive) |
4040
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
4141
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
42-
| `--to` | `string` | | End time (RFC3339, date, or relative) |
42+
| `--to` | `string` | | End time (RFC3339, date, or relative: now, today, tomorrow, monday) |
4343
| `--today` | `bool` | | Today only |
4444
| `--tomorrow` | `bool` | | Tomorrow only |
4545
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |

internal/cmd/calendar_all_events_test.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"net/http"
66
"strings"
77
"testing"
8+
9+
"google.golang.org/api/calendar/v3"
810
)
911

1012
func TestListAllCalendarsEvents_JSON(t *testing.T) {
@@ -60,7 +62,7 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) {
6062
ctx := newCalendarJSONContext(t)
6163

6264
jsonOut := captureStdout(t, func() {
63-
if runErr := listAllCalendarsEvents(ctx, svc, "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z", 10, "", false, false, "", "", "", "", false); runErr != nil {
65+
if runErr := listAllCalendarsEvents(ctx, svc, "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z", 10, "", false, false, "", "", "", "", false, "", ""); runErr != nil {
6466
t.Fatalf("listAllCalendarsEvents: %v", runErr)
6567
}
6668
})
@@ -75,3 +77,108 @@ func TestListAllCalendarsEvents_JSON(t *testing.T) {
7577
t.Fatalf("unexpected events: %#v", parsed.Events)
7678
}
7779
}
80+
81+
// TestListAllCalendarsEvents_SortByStart verifies that --sort=start orders
82+
// events from multiple calendars chronologically (default API order returns
83+
// them grouped per calendar in iteration order).
84+
func TestListAllCalendarsEvents_SortByStart(t *testing.T) {
85+
svc, closeSvc := newCalendarServiceForTest(t, withPrimaryCalendar(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86+
switch {
87+
case strings.Contains(r.URL.Path, "/calendarList") && r.Method == http.MethodGet:
88+
w.Header().Set("Content-Type", "application/json")
89+
_ = json.NewEncoder(w).Encode(map[string]any{
90+
"items": []map[string]any{{"id": "cal1"}, {"id": "cal2"}},
91+
})
92+
return
93+
case strings.Contains(r.URL.Path, "/calendars/cal1/events") && r.Method == http.MethodGet:
94+
w.Header().Set("Content-Type", "application/json")
95+
_ = json.NewEncoder(w).Encode(map[string]any{
96+
"items": []map[string]any{
97+
{
98+
"id": "late", "summary": "Late", "status": "confirmed",
99+
"start": map[string]any{"dateTime": "2025-01-01T15:00:00Z"},
100+
"end": map[string]any{"dateTime": "2025-01-01T16:00:00Z"},
101+
},
102+
},
103+
})
104+
return
105+
case strings.Contains(r.URL.Path, "/calendars/cal2/events") && r.Method == http.MethodGet:
106+
w.Header().Set("Content-Type", "application/json")
107+
_ = json.NewEncoder(w).Encode(map[string]any{
108+
"items": []map[string]any{
109+
{
110+
"id": "early", "summary": "Early", "status": "confirmed",
111+
"start": map[string]any{"dateTime": "2025-01-01T08:00:00Z"},
112+
"end": map[string]any{"dateTime": "2025-01-01T09:00:00Z"},
113+
},
114+
},
115+
})
116+
return
117+
}
118+
http.NotFound(w, r)
119+
})))
120+
defer closeSvc()
121+
122+
ctx := newCalendarJSONContext(t)
123+
jsonOut := captureStdout(t, func() {
124+
if err := listAllCalendarsEvents(ctx, svc, "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z", 10, "", false, false, "", "", "", "", false, "start", "asc"); err != nil {
125+
t.Fatalf("listAllCalendarsEvents: %v", err)
126+
}
127+
})
128+
129+
var parsed struct {
130+
Events []map[string]any `json:"events"`
131+
}
132+
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
133+
t.Fatalf("json parse: %v", err)
134+
}
135+
if len(parsed.Events) != 2 {
136+
t.Fatalf("expected 2 events, got %#v", parsed.Events)
137+
}
138+
if got, _ := parsed.Events[0]["id"].(string); got != "early" {
139+
t.Fatalf("expected first event id 'early', got %q (events: %#v)", got, parsed.Events)
140+
}
141+
if got, _ := parsed.Events[1]["id"].(string); got != "late" {
142+
t.Fatalf("expected second event id 'late', got %q (events: %#v)", got, parsed.Events)
143+
}
144+
145+
// Descending order flips it.
146+
jsonOut = captureStdout(t, func() {
147+
if err := listAllCalendarsEvents(ctx, svc, "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z", 10, "", false, false, "", "", "", "", false, "start", "desc"); err != nil {
148+
t.Fatalf("listAllCalendarsEvents desc: %v", err)
149+
}
150+
})
151+
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
152+
t.Fatalf("json parse desc: %v", err)
153+
}
154+
if got, _ := parsed.Events[0]["id"].(string); got != "late" {
155+
t.Fatalf("desc: expected first id 'late', got %q", got)
156+
}
157+
}
158+
159+
// TestSortEventsBy_Summary verifies case-insensitive summary sort works on
160+
// the wrapper slice independent of API call wiring.
161+
func TestSortEventsBy_Summary(t *testing.T) {
162+
events := []*eventWithCalendar{
163+
{Event: &calendar.Event{Summary: "banana"}},
164+
{Event: &calendar.Event{Summary: "Apple"}},
165+
{Event: &calendar.Event{Summary: "cherry"}},
166+
}
167+
sortEventsBy(events, "summary", "asc")
168+
got := []string{events[0].Summary, events[1].Summary, events[2].Summary}
169+
want := []string{"Apple", "banana", "cherry"}
170+
for i := range got {
171+
if got[i] != want[i] {
172+
t.Fatalf("summary asc: got %v want %v", got, want)
173+
}
174+
}
175+
176+
sortEventsBy(events, "summary", "desc")
177+
got = []string{events[0].Summary, events[1].Summary, events[2].Summary}
178+
want = []string{"cherry", "banana", "Apple"}
179+
for i := range got {
180+
if got[i] != want[i] {
181+
t.Fatalf("summary desc: got %v want %v", got, want)
182+
}
183+
}
184+
}

internal/cmd/calendar_events_cmds.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ type CalendarEventsCmd struct {
1313
CalendarID []string `arg:"" name:"calendarId" optional:"" help:"Calendar ID (default: primary); optional leading list/ls selector is accepted for compatibility"`
1414
Cal []string `name:"cal" help:"Calendar ID or name (can be repeated)"`
1515
Calendars string `name:"calendars" help:"Comma-separated calendar IDs, names, or indices from 'calendar calendars'"`
16-
From string `name:"from" help:"Start time (RFC3339 with timezone, date, or relative: today, tomorrow, monday)"`
17-
To string `name:"to" help:"End time (RFC3339 with timezone, date, or relative)"`
16+
From string `name:"from" help:"Start time (RFC3339 with timezone, date, or relative: now, today, tomorrow, monday)"`
17+
To string `name:"to" help:"End time (RFC3339 with timezone, date, or relative: now, today, tomorrow, monday)"`
1818
Today bool `name:"today" help:"Today only (timezone-aware)"`
1919
Tomorrow bool `name:"tomorrow" help:"Tomorrow only (timezone-aware)"`
2020
Week bool `name:"week" help:"This week (uses --week-start, default Mon)"`
@@ -30,6 +30,8 @@ type CalendarEventsCmd struct {
3030
SharedPropFilter string `name:"shared-prop-filter" help:"Filter by shared extended property (key=value)"`
3131
Fields string `name:"fields" help:"Comma-separated fields to return"`
3232
Weekday bool `name:"weekday" help:"Include start/end day-of-week columns" default:"${calendar_weekday}"`
33+
Sort string `name:"sort" help:"Sort events by start|end|summary|calendar (default: keep API order; with --all, start is recommended for chronological output)" enum:"start,end,summary,calendar," default:""`
34+
Order string `name:"order" help:"Sort order" enum:"asc,desc" default:"asc"`
3335
}
3436

3537
func (c *CalendarEventsCmd) Run(ctx context.Context, flags *RootFlags) error {
@@ -80,7 +82,7 @@ func (c *CalendarEventsCmd) Run(ctx context.Context, flags *RootFlags) error {
8082
from, to := timeRange.FormatRFC3339()
8183

8284
if c.All {
83-
return listAllCalendarsEvents(ctx, svc, from, to, c.Max, c.Page, c.AllPages, c.FailEmpty, c.Query, c.PrivatePropFilter, c.SharedPropFilter, c.Fields, c.Weekday)
85+
return listAllCalendarsEvents(ctx, svc, from, to, c.Max, c.Page, c.AllPages, c.FailEmpty, c.Query, c.PrivatePropFilter, c.SharedPropFilter, c.Fields, c.Weekday, c.Sort, c.Order)
8486
}
8587
if len(calInputs) > 0 {
8688
ids, err := resolveCalendarIDs(ctx, svc, calInputs)
@@ -90,9 +92,9 @@ func (c *CalendarEventsCmd) Run(ctx context.Context, flags *RootFlags) error {
9092
if len(ids) == 0 {
9193
return usage("no calendars specified")
9294
}
93-
return listSelectedCalendarsEvents(ctx, svc, ids, from, to, c.Max, c.Page, c.AllPages, c.FailEmpty, c.Query, c.PrivatePropFilter, c.SharedPropFilter, c.Fields, c.Weekday)
95+
return listSelectedCalendarsEvents(ctx, svc, ids, from, to, c.Max, c.Page, c.AllPages, c.FailEmpty, c.Query, c.PrivatePropFilter, c.SharedPropFilter, c.Fields, c.Weekday, c.Sort, c.Order)
9496
}
95-
return listCalendarEvents(ctx, svc, calendarID, from, to, c.Max, c.Page, c.AllPages, c.FailEmpty, c.Query, c.PrivatePropFilter, c.SharedPropFilter, c.Fields, c.Weekday)
97+
return listCalendarEvents(ctx, svc, calendarID, from, to, c.Max, c.Page, c.AllPages, c.FailEmpty, c.Query, c.PrivatePropFilter, c.SharedPropFilter, c.Fields, c.Weekday, c.Sort, c.Order)
9698
}
9799

98100
func normalizeCalendarEventsArgs(args []string) (string, error) {

internal/cmd/calendar_events_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestListCalendarEvents_JSON(t *testing.T) {
3333
ctx := newCalendarJSONContext(t)
3434

3535
jsonOut := captureStdout(t, func() {
36-
if err := listCalendarEvents(ctx, svc, "cal1", "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z", 10, "", false, false, "", "", "", "", false); err != nil {
36+
if err := listCalendarEvents(ctx, svc, "cal1", "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z", 10, "", false, false, "", "", "", "", false, "", ""); err != nil {
3737
t.Fatalf("listCalendarEvents: %v", err)
3838
}
3939
})
@@ -82,7 +82,7 @@ func TestListCalendarEvents_TableUsesCalendarTimezone(t *testing.T) {
8282

8383
text := captureStdout(t, func() {
8484
ctx := newCalendarOutputContext(t, os.Stdout, io.Discard)
85-
if err := listCalendarEvents(ctx, svc, "cal1", "2026-04-08T00:00:00Z", "2026-04-09T00:00:00Z", 10, "", false, false, "", "", "", "", false); err != nil {
85+
if err := listCalendarEvents(ctx, svc, "cal1", "2026-04-08T00:00:00Z", "2026-04-09T00:00:00Z", 10, "", false, false, "", "", "", "", false, "", ""); err != nil {
8686
t.Fatalf("listCalendarEvents: %v", err)
8787
}
8888
})
@@ -127,7 +127,7 @@ func TestListCalendarEvents_JSONUsesCalendarTimezoneForLocalFields(t *testing.T)
127127

128128
ctx := newCalendarJSONContext(t)
129129
jsonOut := captureStdout(t, func() {
130-
if err := listCalendarEvents(ctx, svc, "cal1", "2026-04-08T00:00:00Z", "2026-04-09T00:00:00Z", 10, "", false, false, "", "", "", "", false); err != nil {
130+
if err := listCalendarEvents(ctx, svc, "cal1", "2026-04-08T00:00:00Z", "2026-04-09T00:00:00Z", 10, "", false, false, "", "", "", "", false, "", ""); err != nil {
131131
t.Fatalf("listCalendarEvents: %v", err)
132132
}
133133
})

0 commit comments

Comments
 (0)