Skip to content
Open
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
6 changes: 6 additions & 0 deletions .agents/skills/gog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ gog --account user@example.com calendar events --today --json --wrap-untrusted
gog --account user@example.com drive ls --max 20 --json --wrap-untrusted
gog --account user@example.com docs cat <documentId> --json --wrap-untrusted
gog --account user@example.com sheets get <spreadsheetId> Sheet1!A1:D20 --json --wrap-untrusted
gog --account user@example.com sheets batch-update <spreadsheetId> --data-json @updates.json --json
gog --account user@example.com contacts list --max 20 --json --wrap-untrusted
```

Expand All @@ -108,12 +109,17 @@ commands that support `--dry-run`, and clean up disposable live-test objects.
```bash
gog --account user@example.com docs write <documentId> --append --text '...'
gog --account user@example.com sheets update <spreadsheetId> Sheet1!A1 --values-json '[["hello"]]'
gog --account user@example.com sheets batch-update <spreadsheetId> --data-json @updates.json
gog --account user@example.com drive upload ./file.txt --parent <folderId> --json
```

When testing creation commands, name artifacts with a clear temporary prefix and
delete or trash them after verification.

For larger Sheets writes, prefer `sheets batch-update` over loops of
`sheets update`; it sends multiple value ranges in one Sheets API request and
accepts inline JSON or `@file` input.

## Discovery

Use generated command docs and schema instead of guessing flags:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Calendar: add --with-zoom / --regenerate-zoom / --remove-zoom that create, regenerate, and remove Zoom meetings and attach the join URL + meeting ID + passcode to the Calendar event description. Google's Calendar API rejects conferenceData writes asserting `conferenceSolution.key.type="addOn"` from non-Workspace-Marketplace OAuth clients, so the description-mode integration is the path that round-trips through Google's storage; trade-off is no native "Join with Zoom" conference card. (#589, #590) — thanks @alexisperumal and @mvanhorn.
- Auth: add gog zoom auth setup / doctor for Zoom S2S OAuth credential storage. (#590) — thanks @mvanhorn.
- Sheets: add `sheets batch-update` for updating multiple value ranges in one Sheets API request. (#601)

### Fixed

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,14 @@ gog docs raw <docId> --pretty

### Sheets

Docs: [Sheets tables](docs/sheets-tables.md),
Docs: [Sheets batch updates](docs/sheets-batch-update.md),
[Sheets tables](docs/sheets-tables.md),
[Sheets formatting](docs/sheets-formatting.md),
[`gog sheets`](docs/commands/gog-sheets.md).

```bash
gog sheets get <spreadsheetId> 'Sheet1!A1:D20' --json
gog sheets batch-update <spreadsheetId> --data-json @updates.json --json
gog sheets table list <spreadsheetId>
gog sheets table append <spreadsheetId> Tasks 'Ship README|done'
gog sheets table clear <spreadsheetId> Tasks
Expand Down
1 change: 1 addition & 0 deletions docs/commands.generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ Generated from `gog schema --json`.
- [`gog sheets (sheet) banding (banded-ranges) clear (delete,rm,remove) <spreadsheetId> [flags]`](commands/gog-sheets-banding-clear.md) - Remove alternating color banding
- [`gog sheets (sheet) banding (banded-ranges) list <spreadsheetId> [flags]`](commands/gog-sheets-banding-list.md) - List alternating color banded ranges
- [`gog sheets (sheet) banding (banded-ranges) set (add,create) <spreadsheetId> <range> [flags]`](commands/gog-sheets-banding-set.md) - Apply alternating colors to a range
- [`gog sheets (sheet) batch-update (batch) --data-json=STRING <spreadsheetId> [flags]`](commands/gog-sheets-batch-update.md) - Update values in multiple ranges with one API request
- [`gog sheets (sheet) chart (charts) <command>`](commands/gog-sheets-chart.md) - Manage spreadsheet charts
- [`gog sheets (sheet) chart (charts) create (add,new) --spec-json=STRING <spreadsheetId> [flags]`](commands/gog-sheets-chart-create.md) - Create a chart from a JSON spec
- [`gog sheets (sheet) chart (charts) delete (rm,remove,del) <spreadsheetId> <chartId>`](commands/gog-sheets-chart-delete.md) - Delete a chart
Expand Down
3 changes: 2 additions & 1 deletion docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Every `gog` command has a generated docs page. The source of truth is the live CLI schema; run `make docs-commands` after changing command names, flags, help text, aliases, or arguments.

Generated pages: 564.
Generated pages: 565.

## Top-level Commands

Expand Down Expand Up @@ -515,6 +515,7 @@ Generated pages: 564.
- [gog sheets banding clear](gog-sheets-banding-clear.md) - Remove alternating color banding
- [gog sheets banding list](gog-sheets-banding-list.md) - List alternating color banded ranges
- [gog sheets banding set](gog-sheets-banding-set.md) - Apply alternating colors to a range
- [gog sheets batch-update](gog-sheets-batch-update.md) - Update values in multiple ranges with one API request
- [gog sheets chart](gog-sheets-chart.md) - Manage spreadsheet charts
- [gog sheets chart create](gog-sheets-chart-create.md) - Create a chart from a JSON spec
- [gog sheets chart delete](gog-sheets-chart-delete.md) - Delete a chart
Expand Down
48 changes: 48 additions & 0 deletions docs/commands/gog-sheets-batch-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# `gog sheets batch-update`

> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.

Update values in multiple ranges with one API request

## Usage

```bash
gog sheets (sheet) batch-update (batch) --data-json=STRING <spreadsheetId> [flags]
```

## Parent

- [gog sheets](gog-sheets.md)

## Flags

| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/drivelabels/docs/slides/contacts/tasks/people/sheets/forms/sites/appscript/analytics/searchconsole/ads/photos) |
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
| `--color` | `string` | auto | Color output: auto\|always\|never |
| `--data-json` | `string` | | Value ranges as JSON array, or @file (e.g. [{"range":"Sheet1!A1:B2","values":[["a","b"]]}]) |
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `--include-values-in-response` | `bool` | | Include updated values in the response |
| `--input` | `string` | USER_ENTERED | Value input option: RAW or USER_ENTERED |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--response-date-time-render` | `string` | | Response date/time render option: SERIAL_NUMBER or FORMATTED_STRING |
| `--response-render` | `string` | | Response value render option: FORMATTED_VALUE, UNFORMATTED_VALUE, or FORMULA |
| `--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. |
| `-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 |

## See Also

- [gog sheets](gog-sheets.md)
- [Command index](README.md)
1 change: 1 addition & 0 deletions docs/commands/gog-sheets.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ gog sheets (sheet) <command> [flags]
- [gog sheets add-tab](gog-sheets-add-tab.md) - Add a new tab/sheet to a spreadsheet
- [gog sheets append](gog-sheets-append.md) - Append values to a range
- [gog sheets banding](gog-sheets-banding.md) - Manage alternating color banding
- [gog sheets batch-update](gog-sheets-batch-update.md) - Update values in multiple ranges with one API request
- [gog sheets chart](gog-sheets-chart.md) - Manage spreadsheet charts
- [gog sheets clear](gog-sheets-clear.md) - Clear values in a range
- [gog sheets conditional-format](gog-sheets-conditional-format.md) - Manage conditional formatting rules
Expand Down
54 changes: 54 additions & 0 deletions docs/sheets-batch-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Sheets Batch Updates

Use `gog sheets batch-update` when you need to update multiple ranges in the
same spreadsheet without making one API call per range. The command sends a
single Google Sheets `spreadsheets.values.batchUpdate` request.

Prepare a JSON array of value ranges:

```json
[
{
"range": "Sheet1!A1:B1",
"values": [["Name", "Status"]]
},
{
"range": "Sheet1!A2:B3",
"values": [
["Ada", "Ready"],
["Grace", "Blocked"]
]
}
]
```

Then pass it inline or from a file:

```bash
gog sheets batch-update "$spreadsheet_id" --data-json @updates.json --json
```

By default, values are interpreted as if they were entered in the Google Sheets
UI (`USER_ENTERED`). Use `--input RAW` to store values without parsing:

```bash
gog sheets batch-update "$spreadsheet_id" \
--input RAW \
--data-json '[{"range":"Sheet1!A1:B1","values":[["001","plain text"]]}]'
```

Add `--include-values-in-response` when callers need the post-update cell values
back from Google:

```bash
gog sheets batch-update "$spreadsheet_id" \
--include-values-in-response \
--response-render UNFORMATTED_VALUE \
--data-json @updates.json \
--json
```

Related command reference:

- [`gog sheets batch-update`](commands/gog-sheets-batch-update.md)
- [`gog sheets update`](commands/gog-sheets-update.md)
2 changes: 2 additions & 0 deletions internal/cmd/docs_format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestDocsFormatFlagsBuildRequests(t *testing.T) {
textReq := reqs[0].UpdateTextStyle
if textReq == nil {
t.Fatalf("missing text request: %#v", reqs[0])
return
}
if got := textReq.Range; got.StartIndex != 3 || got.EndIndex != 9 || got.TabId != "t.second" {
t.Fatalf("unexpected text range: %#v", got)
Expand All @@ -56,6 +57,7 @@ func TestDocsFormatFlagsBuildRequests(t *testing.T) {
paraReq := reqs[1].UpdateParagraphStyle
if paraReq == nil {
t.Fatalf("missing paragraph request: %#v", reqs[1])
return
}
if paraReq.ParagraphStyle.Alignment != "CENTER" || paraReq.ParagraphStyle.LineSpacing != 150 {
t.Fatalf("unexpected paragraph style: %#v", paraReq.ParagraphStyle)
Expand Down
110 changes: 110 additions & 0 deletions internal/cmd/sheets.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func cleanRange(r string) string {
type SheetsCmd struct {
Get SheetsGetCmd `cmd:"" name:"get" aliases:"read,show" help:"Get values from a range"`
Update SheetsUpdateCmd `cmd:"" name:"update" aliases:"edit,set" help:"Update values in a range"`
BatchUpdate SheetsBatchUpdateCmd `cmd:"" name:"batch-update" aliases:"batch" help:"Update values in multiple ranges with one API request"`
Append SheetsAppendCmd `cmd:"" name:"append" aliases:"add" help:"Append values to a range"`
Insert SheetsInsertCmd `cmd:"" name:"insert" help:"Insert empty rows or columns into a sheet"`
Clear SheetsClearCmd `cmd:"" name:"clear" help:"Clear values in a range"`
Expand Down Expand Up @@ -262,6 +263,115 @@ func (c *SheetsUpdateCmd) Run(ctx context.Context, flags *RootFlags) error {
return nil
}

type SheetsBatchUpdateCmd struct {
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
DataJSON string `name:"data-json" required:"" help:"Value ranges as JSON array, or @file (e.g. [{\"range\":\"Sheet1!A1:B2\",\"values\":[[\"a\",\"b\"]]}])"`
ValueInput string `name:"input" help:"Value input option: RAW or USER_ENTERED" default:"USER_ENTERED"`
IncludeValuesInResponse bool `name:"include-values-in-response" help:"Include updated values in the response"`
ResponseValueRenderOption string `name:"response-render" help:"Response value render option: FORMATTED_VALUE, UNFORMATTED_VALUE, or FORMULA"`
ResponseDateTimeRenderOption string `name:"response-date-time-render" help:"Response date/time render option: SERIAL_NUMBER or FORMATTED_STRING"`
}

func (c *SheetsBatchUpdateCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)

spreadsheetID := normalizeGoogleID(strings.TrimSpace(c.SpreadsheetID))
if spreadsheetID == "" {
return usage("empty spreadsheetId")
}

data, err := parseSheetsBatchUpdateData(c.DataJSON)
if err != nil {
return err
}

valueInputOption := strings.TrimSpace(c.ValueInput)
if valueInputOption == "" {
valueInputOption = sheetsDefaultValueInputOption
}
req := &sheets.BatchUpdateValuesRequest{
Data: data,
ValueInputOption: valueInputOption,
IncludeValuesInResponse: c.IncludeValuesInResponse,
}
if strings.TrimSpace(c.ResponseValueRenderOption) != "" {
req.ResponseValueRenderOption = strings.TrimSpace(c.ResponseValueRenderOption)
}
if strings.TrimSpace(c.ResponseDateTimeRenderOption) != "" {
req.ResponseDateTimeRenderOption = strings.TrimSpace(c.ResponseDateTimeRenderOption)
}

if dryRunErr := dryRunExit(ctx, flags, "sheets.batch-update", map[string]any{
"spreadsheet_id": spreadsheetID,
"value_input_option": req.ValueInputOption,
"include_values_in_response": req.IncludeValuesInResponse,
"response_value_render_option": req.ResponseValueRenderOption,
"response_date_time_render_option": req.ResponseDateTimeRenderOption,
"data": req.Data,
}); dryRunErr != nil {
return dryRunErr
}

account, err := requireAccount(flags)
if err != nil {
return err
}

svc, err := newSheetsService(ctx, account)
if err != nil {
return err
}

resp, err := svc.Spreadsheets.Values.BatchUpdate(spreadsheetID, req).Do()
if err != nil {
return err
}

if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"spreadsheetId": resp.SpreadsheetId,
"totalUpdatedRows": resp.TotalUpdatedRows,
"totalUpdatedColumns": resp.TotalUpdatedColumns,
"totalUpdatedCells": resp.TotalUpdatedCells,
"totalUpdatedSheets": resp.TotalUpdatedSheets,
"responses": resp.Responses,
})
}

u.Out().Linef("Updated %d cells across %d ranges in %s", resp.TotalUpdatedCells, len(resp.Responses), spreadsheetID)
return nil
}

func parseSheetsBatchUpdateData(dataJSON string) ([]*sheets.ValueRange, error) {
if strings.TrimSpace(dataJSON) == "" {
return nil, usage("empty data-json")
}
b, err := resolveInlineOrFileBytes(dataJSON)
if err != nil {
return nil, fmt.Errorf("read --data-json: %w", err)
}
var data []*sheets.ValueRange
if unmarshalErr := json.Unmarshal(b, &data); unmarshalErr != nil {
return nil, fmt.Errorf("invalid JSON data: %w", unmarshalErr)
}
if len(data) == 0 {
return nil, usage("--data-json must contain at least one value range")
}
for i, vr := range data {
if vr == nil {
return nil, usagef("--data-json range %d is null", i)
}
vr.Range = cleanRange(vr.Range)
if strings.TrimSpace(vr.Range) == "" {
return nil, usagef("--data-json range %d has empty range", i)
}
if vr.Values == nil {
return nil, usagef("--data-json range %d has empty values", i)
}
}
return data, nil
}

type SheetsAppendCmd struct {
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
Range string `arg:"" name:"range" help:"Range (A1 notation or named range name; e.g. Sheet1!A:C or MyNamedRange)"`
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/sheets_advanced_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func TestSheetsConditionalClearAllDeletesReverseAndRequiresForce(t *testing.T) {
second := (*requests)[0].Requests[1].DeleteConditionalFormatRule
if first == nil || second == nil {
t.Fatalf("missing deleteConditionalFormatRule: %#v", (*requests)[0].Requests)
return
}
if first.Index != 1 || second.Index != 0 {
t.Fatalf("delete indexes = %d,%d; want 1,0", first.Index, second.Index)
Expand Down
Loading