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 @@ -4,6 +4,7 @@

### Added

- Drive/Docs: add `--since` to `drive comments list` and `docs comments list` for server-side comment modified-time filtering. (#688) — thanks @sebsnyk.
- Gmail: add `--thread-id` to `gmail drafts create` and `gmail drafts update` so drafts can reply within a thread using the latest message headers. (#673, #674) — thanks @chrischall.

### Fixed
Expand Down
1 change: 1 addition & 0 deletions docs/commands/gog-docs-comments-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ gog docs (doc) comments list (ls) <docId> [flags]
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--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. |
| `--since` | `string` | | Only return comments modified at or after this RFC3339 timestamp |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
Expand Down
1 change: 1 addition & 0 deletions docs/commands/gog-drive-comments-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ gog drive (drv) comments list (ls) <fileId> [flags]
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--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. |
| `--since` | `string` | | Only return comments modified at or after this RFC3339 timestamp |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
Expand Down
21 changes: 19 additions & 2 deletions internal/cmd/comment_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"strings"
"time"

"google.golang.org/api/drive/v3"
gapi "google.golang.org/api/googleapi"
Expand Down Expand Up @@ -61,6 +62,7 @@ type driveCommentListOptions struct {
includeQuoted bool
scanForOpen bool
page string
since string
all bool
failEmpty bool
max int64
Expand All @@ -70,7 +72,7 @@ type driveCommentListOptions struct {

func listDriveComments(ctx context.Context, svc *drive.Service, fileID string, opts driveCommentListOptions) ([]*drive.Comment, string, error) {
fetch := func(pageToken string) ([]*drive.Comment, string, error) {
return fetchDriveCommentsPage(ctx, svc, fileID, opts.max, pageToken, driveCommentFieldsForList(opts))
return fetchDriveCommentsPage(ctx, svc, fileID, opts.max, pageToken, opts.since, driveCommentFieldsForList(opts))
}

if opts.all {
Expand Down Expand Up @@ -106,7 +108,7 @@ func listDriveComments(ctx context.Context, svc *drive.Service, fileID string, o
}
}

func fetchDriveCommentsPage(ctx context.Context, svc *drive.Service, fileID string, pageSize int64, pageToken string, commentFields string) ([]*drive.Comment, string, error) {
func fetchDriveCommentsPage(ctx context.Context, svc *drive.Service, fileID string, pageSize int64, pageToken string, since string, commentFields string) ([]*drive.Comment, string, error) {
call := svc.Comments.List(fileID).
IncludeDeleted(false).
PageSize(pageSize).
Expand All @@ -115,13 +117,28 @@ func fetchDriveCommentsPage(ctx context.Context, svc *drive.Service, fileID stri
if strings.TrimSpace(pageToken) != "" {
call = call.PageToken(pageToken)
}
if strings.TrimSpace(since) != "" {
call = call.StartModifiedTime(since)
}
resp, err := call.Do()
if err != nil {
return nil, "", err
}
return resp.Comments, resp.NextPageToken, nil
}

func normalizeDriveCommentSince(raw string) (string, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return "", nil
}
parsed, err := time.Parse(time.RFC3339Nano, trimmed)
if err != nil {
return "", usagef("invalid --since %q (expected RFC3339 timestamp with timezone)", raw)
}
return parsed.Format(time.RFC3339Nano), nil
}

func driveCommentFieldsForList(opts driveCommentListOptions) string {
if opts.mode == driveCommentListModeExpanded {
return docsCommentListFields
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/docs_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type DocsCommentsListCmd struct {
Page string `name:"page" aliases:"cursor" help:"Page token for pagination"`
All bool `name:"all" aliases:"all-pages" help:"Fetch all pages"`
FailEmpty bool `name:"fail-empty" aliases:"non-empty,require-results" help:"Exit with code 3 if no results"`
Since string `name:"since" help:"Only return comments modified at or after this RFC3339 timestamp"`
}

func (c *DocsCommentsListCmd) Run(ctx context.Context, flags *RootFlags) error {
Expand All @@ -39,6 +40,10 @@ func (c *DocsCommentsListCmd) Run(ctx context.Context, flags *RootFlags) error {
if c.Max <= 0 {
return usage("max must be > 0")
}
since, err := normalizeDriveCommentSince(c.Since)
if err != nil {
return err
}

_, svc, err := requireDriveService(ctx, flags)
if err != nil {
Expand All @@ -50,6 +55,7 @@ func (c *DocsCommentsListCmd) Run(ctx context.Context, flags *RootFlags) error {
includeResolved: c.IncludeResolved,
scanForOpen: true,
page: c.Page,
since: since,
all: c.All,
failEmpty: c.FailEmpty,
max: c.Max,
Expand Down
47 changes: 47 additions & 0 deletions internal/cmd/docs_comments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ func newCommentsTestServer(t *testing.T) *httptest.Server {
})
return

// List comments with modified-time filter.
case r.Method == http.MethodGet && path == "/files/doc-since/comments":
if r.URL.Query().Get("startModifiedTime") != "2026-06-04T10:00:00Z" {
t.Fatalf("expected startModifiedTime, got: %q", r.URL.RawQuery)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"comments": []map[string]any{
{
"id": "c-since",
"author": map[string]any{"displayName": "Alice"},
"content": "Fresh",
"createdTime": "2026-06-04T10:00:01Z",
"modifiedTime": "2026-06-04T10:00:01Z",
"resolved": false,
},
},
})
return

// List comments: first page has only resolved, second page has open.
case r.Method == http.MethodGet && path == "/files/scan/comments":
w.Header().Set("Content-Type", "application/json")
Expand Down Expand Up @@ -244,6 +264,30 @@ func TestDocsCommentsList_IncludeResolved(t *testing.T) {
}
}

func TestDocsCommentsList_Since(t *testing.T) {
srv := newCommentsTestServer(t)
defer srv.Close()
setupDriveServiceFromServer(t, srv)

jsonOut := captureStdout(t, func() {
_ = captureStderr(t, func() {
if err := Execute([]string{"--json", "--account", "a@b.com", "docs", "comments", "list", "--since", "2026-06-04T10:00:00Z", "doc-since"}); err != nil {
t.Fatalf("Execute: %v", err)
}
})
})

var parsed struct {
Comments []*drive.Comment `json:"comments"`
}
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
t.Fatalf("json parse: %v\nout=%q", err, jsonOut)
}
if len(parsed.Comments) != 1 || parsed.Comments[0].Id != "c-since" {
t.Fatalf("unexpected comments: %#v", parsed.Comments)
}
}

func TestDocsCommentsList_PlainText(t *testing.T) {
srv := newCommentsTestServer(t)
defer srv.Close()
Expand Down Expand Up @@ -525,6 +569,9 @@ func TestDocsComments_ValidationErrors(t *testing.T) {
if err := (&DocsCommentsListCmd{}).Run(ctx, flags); err == nil {
t.Fatal("expected list missing docId error")
}
if err := (&DocsCommentsListCmd{DocID: "d1", Max: 1, Since: "2026-06-04T10:00:00"}).Run(ctx, flags); err == nil {
t.Fatal("expected list invalid since error")
}
if err := (&DocsCommentsGetCmd{}).Run(ctx, flags); err == nil {
t.Fatal("expected get missing docId error")
}
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/drive_comments.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type DriveCommentsListCmd struct {
All bool `name:"all" aliases:"all-pages,allpages" help:"Fetch all pages"`
FailEmpty bool `name:"fail-empty" aliases:"non-empty,require-results" help:"Exit with code 3 if no results"`
IncludeQuoted bool `name:"include-quoted" help:"Include the quoted content the comment is anchored to"`
Since string `name:"since" help:"Only return comments modified at or after this RFC3339 timestamp"`
}

func (c *DriveCommentsListCmd) Run(ctx context.Context, flags *RootFlags) error {
Expand All @@ -38,6 +39,10 @@ func (c *DriveCommentsListCmd) Run(ctx context.Context, flags *RootFlags) error
if c.Max <= 0 {
return usage("max must be > 0")
}
since, err := normalizeDriveCommentSince(c.Since)
if err != nil {
return err
}

_, svc, err := requireDriveService(ctx, flags)
if err != nil {
Expand All @@ -48,6 +53,7 @@ func (c *DriveCommentsListCmd) Run(ctx context.Context, flags *RootFlags) error
resourceID: fileID,
includeQuoted: c.IncludeQuoted,
page: c.Page,
since: since,
all: c.All,
failEmpty: c.FailEmpty,
max: c.Max,
Expand Down
7 changes: 5 additions & 2 deletions internal/cmd/drive_comments_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ func TestDriveCommentsListCmd_TextAndJSON(t *testing.T) {
if r.URL.Query().Get("pageToken") != "p1" {
t.Fatalf("expected pageToken=p1, got: %q", r.URL.RawQuery)
}
if r.URL.Query().Get("startModifiedTime") != "2026-06-04T10:00:00Z" {
t.Fatalf("expected startModifiedTime, got: %q", r.URL.RawQuery)
}
fields := r.URL.Query().Get("fields")
if !strings.Contains(fields, "quotedFileContent") {
t.Fatalf("expected quotedFileContent in fields, got: %q", fields)
Expand Down Expand Up @@ -82,7 +85,7 @@ func TestDriveCommentsListCmd_TextAndJSON(t *testing.T) {

textOut := captureStdout(t, func() {
cmd := &DriveCommentsListCmd{}
if execErr := runKong(t, cmd, []string{"--max", "1", "--page", "p1", "--include-quoted", "id1"}, ctx, flags); execErr != nil {
if execErr := runKong(t, cmd, []string{"--max", "1", "--page", "p1", "--since", "2026-06-04T10:00:00Z", "--include-quoted", "id1"}, ctx, flags); execErr != nil {
t.Fatalf("execute: %v", execErr)
}
})
Expand All @@ -103,7 +106,7 @@ func TestDriveCommentsListCmd_TextAndJSON(t *testing.T) {

jsonOut := captureStdout(t, func() {
cmd := &DriveCommentsListCmd{}
if execErr := runKong(t, cmd, []string{"--max", "1", "--page", "p1", "--include-quoted", "id1"}, ctx2, flags); execErr != nil {
if execErr := runKong(t, cmd, []string{"--max", "1", "--page", "p1", "--since", "2026-06-04T10:00:00Z", "--include-quoted", "id1"}, ctx2, flags); execErr != nil {
t.Fatalf("execute: %v", execErr)
}
})
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/drive_comments_validation_more_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ func TestDriveComments_ValidationErrors(t *testing.T) {
if err := (&DriveCommentsListCmd{FileID: "f1", Max: 0}).Run(ctx, flags); err == nil {
t.Fatalf("expected list max error")
}
if err := (&DriveCommentsListCmd{FileID: "f1", Max: 1, Since: "2026-06-04T10:00:00"}).Run(ctx, flags); err == nil {
t.Fatalf("expected list since error")
}
if err := (&DriveCommentsGetCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected get missing fileId error")
}
Expand Down
Loading