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

Skip to content

Commit f911d6b

Browse files
authored
feat(sheets): add batch value updates
Add `gog sheets batch-update` for multi-range value writes through one Sheets `values.batchUpdate` request. Maintainer fixups: - Kept the `sheets batch-update` syntax with `batch` alias to match the Sheets API and existing command naming. - Resolved current-main changelog/docs conflicts and regenerated command docs. - Added contributor credit and tightened validation so empty value arrays are rejected. Proof: - `go test ./internal/cmd -run 'TestSheetsBatchUpdate|TestSheetsCommands_JSON|TestSheetsCommands_Text|TestSafetyProfile|TestParseSheetsBatchUpdateDataRejectsInvalidPayloads' -count=1` - `make ci` - `codex-review --mode branch --parallel-tests "go test ./internal/cmd -run 'TestSheetsBatchUpdate|TestSheetsCommands_JSON|TestSheetsCommands_Text|TestSafetyProfile|TestParseSheetsBatchUpdateDataRejectsInvalidPayloads' -count=1"` - Live Google Sheets test with `[email protected]`: created disposable spreadsheet `1YGLDGBeD82aTOt-1LtQTWSgA-m69TQlWhqV3MyM5k-U`, ran `sheets batch-update` over `Sheet1!A1:B2` and `Sheet1!D1:E2`, verified readback from `Sheet1!A1:E2` matched the two written ranges with the blank middle column, then trashed the spreadsheet. - GitHub CI and docker checks green on `420ab406be8750b48dbb21d8109f51fb31c98fda`. Co-authored-by: Martin Kask <[email protected]>
1 parent 54ac2f5 commit f911d6b

17 files changed

Lines changed: 422 additions & 3 deletions

.agents/skills/gog/SKILL.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ gog --account [email protected] calendar events --today --json --wrap-untrusted
106106
gog --account [email protected] drive ls --max 20 --json --wrap-untrusted
107107
gog --account [email protected] docs cat <documentId> --json --wrap-untrusted
108108
gog --account [email protected] sheets get <spreadsheetId> Sheet1!A1:D20 --json --wrap-untrusted
109+
gog --account [email protected] sheets batch-update <spreadsheetId> --data-json @updates.json --json
109110
gog --account [email protected] contacts list --max 20 --json --wrap-untrusted
110111
```
111112

@@ -120,12 +121,17 @@ commands that support `--dry-run`, and clean up disposable live-test objects.
120121
```bash
121122
gog --account [email protected] docs write <documentId> --append --text '...'
122123
gog --account [email protected] sheets update <spreadsheetId> Sheet1!A1 --values-json '[["hello"]]'
124+
gog --account [email protected] sheets batch-update <spreadsheetId> --data-json @updates.json
123125
gog --account [email protected] drive upload ./file.txt --parent <folderId> --json
124126
```
125127

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

131+
For larger Sheets writes, prefer `sheets batch-update` over loops of
132+
`sheets update`; it sends multiple value ranges in one Sheets API request and
133+
accepts inline JSON or `@file` input.
134+
129135
## Discovery
130136

131137
Use generated command docs and schema instead of guessing flags:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- 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.
99
- Auth: add gog zoom auth setup / doctor for Zoom S2S OAuth credential storage. (#590) — thanks @mvanhorn.
1010
- Drive: add `--action=resolve|reopen` to `drive comments reply` and sibling `drive comments resolve|reopen` verbs (also `docs comments reopen`) to post a reply that atomically flips the parent comment's resolved state via the Drive API's `Reply.action` field. Avoids the previous workaround of `drive comments delete` (which destroys review-thread context) for batch-resolving inline doc-review feedback. (#623) — thanks @sebsnyk.
11+
- Sheets: add `gog sheets batch-update <spreadsheetId> --data-json ...` for updating multiple value ranges in one Sheets API request. Alias: `batch`. (#601) — thanks @Tsopic.
1112
- Docs: add `gog docs insert-page-break <docId> [--index N | --at-end] [--tab=STRING]` to insert a Google Docs page break directly via `InsertPageBreakRequest` — markdown has no native page-break construct, so this is the only path for multi-page deliverables. Aliases: `page-break`, `pb`. (#604)
1213
- Docs: add `gog docs page-layout <docId> [--layout=pageless|pages]` to toggle the page layout of an existing Google Doc via `updateDocumentStyle` on `documentFormat.documentMode`. Sibling to the existing `--pageless` flag on `docs create`/`write`/`update` for the case where the doc was created upstream (e.g. by Drive markdown conversion) without the desired layout. Defaults to `pageless`. Aliases: `set-page-layout`, `page-setup`. (#593)
1314
- Docs: add `--heading-level N` (1..6 shortcut) and `--named-style NAME` (full enum) to `gog docs format` so existing paragraphs can be promoted to `HEADING_1`..`HEADING_6`, `TITLE`, `SUBTITLE`, or `NORMAL_TEXT`. Both set `paragraphStyle.namedStyleType` on the existing UpdateParagraphStyle request and compose cleanly with `--alignment` / `--line-spacing`. (#605)

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,14 @@ gog docs raw <docId> --pretty
270270

271271
### Sheets
272272

273-
Docs: [Sheets tables](docs/sheets-tables.md),
273+
Docs: [Sheets batch updates](docs/sheets-batch-update.md),
274+
[Sheets tables](docs/sheets-tables.md),
274275
[Sheets formatting](docs/sheets-formatting.md),
275276
[`gog sheets`](docs/commands/gog-sheets.md).
276277

277278
```bash
278279
gog sheets get <spreadsheetId> 'Sheet1!A1:D20' --json
280+
gog sheets batch-update <spreadsheetId> --data-json @updates.json --json
279281
gog sheets table list <spreadsheetId>
280282
gog sheets table append <spreadsheetId> Tasks 'Ship README|done'
281283
gog sheets table clear <spreadsheetId> Tasks

docs/commands.generated.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ Generated from `gog schema --json`.
470470
- [`gog sheets (sheet) banding (banded-ranges) clear (delete,rm,remove) <spreadsheetId> [flags]`](commands/gog-sheets-banding-clear.md) - Remove alternating color banding
471471
- [`gog sheets (sheet) banding (banded-ranges) list <spreadsheetId> [flags]`](commands/gog-sheets-banding-list.md) - List alternating color banded ranges
472472
- [`gog sheets (sheet) banding (banded-ranges) set (add,create) <spreadsheetId> <range> [flags]`](commands/gog-sheets-banding-set.md) - Apply alternating colors to a range
473+
- [`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
473474
- [`gog sheets (sheet) chart (charts) <command>`](commands/gog-sheets-chart.md) - Manage spreadsheet charts
474475
- [`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
475476
- [`gog sheets (sheet) chart (charts) delete (rm,remove,del) <spreadsheetId> <chartId>`](commands/gog-sheets-chart-delete.md) - Delete a chart

docs/commands/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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.
44

5-
Generated pages: 571.
5+
Generated pages: 572.
66

77
## Top-level Commands
88

@@ -521,6 +521,7 @@ Generated pages: 571.
521521
- [gog sheets banding clear](gog-sheets-banding-clear.md) - Remove alternating color banding
522522
- [gog sheets banding list](gog-sheets-banding-list.md) - List alternating color banded ranges
523523
- [gog sheets banding set](gog-sheets-banding-set.md) - Apply alternating colors to a range
524+
- [gog sheets batch-update](gog-sheets-batch-update.md) - Update values in multiple ranges with one API request
524525
- [gog sheets chart](gog-sheets-chart.md) - Manage spreadsheet charts
525526
- [gog sheets chart create](gog-sheets-chart-create.md) - Create a chart from a JSON spec
526527
- [gog sheets chart delete](gog-sheets-chart-delete.md) - Delete a chart
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# `gog sheets batch-update`
2+
3+
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
4+
5+
Update values in multiple ranges with one API request
6+
7+
## Usage
8+
9+
```bash
10+
gog sheets (sheet) batch-update (batch) --data-json=STRING <spreadsheetId> [flags]
11+
```
12+
13+
## Parent
14+
15+
- [gog sheets](gog-sheets.md)
16+
17+
## Flags
18+
19+
| Flag | Type | Default | Help |
20+
| --- | --- | --- | --- |
21+
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
22+
| `-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) |
23+
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
24+
| `--color` | `string` | auto | Color output: auto\|always\|never |
25+
| `--data-json` | `string` | | Value ranges as JSON array, or @file (e.g. [{"range":"Sheet1!A1:B2","values":[["a","b"]]}]) |
26+
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
27+
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
28+
| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
29+
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
30+
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
31+
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
32+
| `--include-values-in-response` | `bool` | | Include updated values in the response |
33+
| `--input` | `string` | USER_ENTERED | Value input option: RAW or USER_ENTERED |
34+
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
35+
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
36+
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
37+
| `--response-date-time-render` | `string` | | Response date/time render option: SERIAL_NUMBER or FORMATTED_STRING |
38+
| `--response-render` | `string` | | Response value render option: FORMATTED_VALUE, UNFORMATTED_VALUE, or FORMULA |
39+
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
40+
| `--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. |
41+
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
42+
| `--version` | `kong.VersionFlag` | | Print version and exit |
43+
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
44+
45+
## See Also
46+
47+
- [gog sheets](gog-sheets.md)
48+
- [Command index](README.md)

docs/commands/gog-sheets.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ gog sheets (sheet) <command> [flags]
1919
- [gog sheets add-tab](gog-sheets-add-tab.md) - Add a new tab/sheet to a spreadsheet
2020
- [gog sheets append](gog-sheets-append.md) - Append values to a range
2121
- [gog sheets banding](gog-sheets-banding.md) - Manage alternating color banding
22+
- [gog sheets batch-update](gog-sheets-batch-update.md) - Update values in multiple ranges with one API request
2223
- [gog sheets chart](gog-sheets-chart.md) - Manage spreadsheet charts
2324
- [gog sheets clear](gog-sheets-clear.md) - Clear values in a range
2425
- [gog sheets conditional-format](gog-sheets-conditional-format.md) - Manage conditional formatting rules

docs/sheets-batch-update.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Sheets Batch Updates
2+
3+
Use `gog sheets batch-update` when you need to update multiple ranges in the
4+
same spreadsheet without making one API call per range. The command sends a
5+
single Google Sheets `spreadsheets.values.batchUpdate` request.
6+
7+
Prepare a JSON array of value ranges:
8+
9+
```json
10+
[
11+
{
12+
"range": "Sheet1!A1:B1",
13+
"values": [["Name", "Status"]]
14+
},
15+
{
16+
"range": "Sheet1!A2:B3",
17+
"values": [
18+
["Ada", "Ready"],
19+
["Grace", "Blocked"]
20+
]
21+
}
22+
]
23+
```
24+
25+
Then pass it inline or from a file:
26+
27+
```bash
28+
gog sheets batch-update "$spreadsheet_id" --data-json @updates.json --json
29+
```
30+
31+
By default, values are interpreted as if they were entered in the Google Sheets
32+
UI (`USER_ENTERED`). Use `--input RAW` to store values without parsing:
33+
34+
```bash
35+
gog sheets batch-update "$spreadsheet_id" \
36+
--input RAW \
37+
--data-json '[{"range":"Sheet1!A1:B1","values":[["001","plain text"]]}]'
38+
```
39+
40+
Add `--include-values-in-response` when callers need the post-update cell values
41+
back from Google:
42+
43+
```bash
44+
gog sheets batch-update "$spreadsheet_id" \
45+
--include-values-in-response \
46+
--response-render UNFORMATTED_VALUE \
47+
--data-json @updates.json \
48+
--json
49+
```
50+
51+
Related command reference:
52+
53+
- [`gog sheets batch-update`](commands/gog-sheets-batch-update.md)
54+
- [`gog sheets update`](commands/gog-sheets-update.md)

internal/cmd/docs_format_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func TestDocsFormatFlagsBuildRequests(t *testing.T) {
3131
textReq := reqs[0].UpdateTextStyle
3232
if textReq == nil {
3333
t.Fatalf("missing text request: %#v", reqs[0])
34+
return
3435
}
3536
if got := textReq.Range; got.StartIndex != 3 || got.EndIndex != 9 || got.TabId != "t.second" {
3637
t.Fatalf("unexpected text range: %#v", got)
@@ -56,6 +57,7 @@ func TestDocsFormatFlagsBuildRequests(t *testing.T) {
5657
paraReq := reqs[1].UpdateParagraphStyle
5758
if paraReq == nil {
5859
t.Fatalf("missing paragraph request: %#v", reqs[1])
60+
return
5961
}
6062
if paraReq.ParagraphStyle.Alignment != "CENTER" || paraReq.ParagraphStyle.LineSpacing != 150 {
6163
t.Fatalf("unexpected paragraph style: %#v", paraReq.ParagraphStyle)

internal/cmd/sheets.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func cleanRange(r string) string {
2828
type SheetsCmd struct {
2929
Get SheetsGetCmd `cmd:"" name:"get" aliases:"read,show" help:"Get values from a range"`
3030
Update SheetsUpdateCmd `cmd:"" name:"update" aliases:"edit,set" help:"Update values in a range"`
31+
BatchUpdate SheetsBatchUpdateCmd `cmd:"" name:"batch-update" aliases:"batch" help:"Update values in multiple ranges with one API request"`
3132
Append SheetsAppendCmd `cmd:"" name:"append" aliases:"add" help:"Append values to a range"`
3233
Insert SheetsInsertCmd `cmd:"" name:"insert" help:"Insert empty rows or columns into a sheet"`
3334
Clear SheetsClearCmd `cmd:"" name:"clear" help:"Clear values in a range"`
@@ -263,6 +264,115 @@ func (c *SheetsUpdateCmd) Run(ctx context.Context, flags *RootFlags) error {
263264
return nil
264265
}
265266

267+
type SheetsBatchUpdateCmd struct {
268+
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
269+
DataJSON string `name:"data-json" required:"" help:"Value ranges as JSON array, or @file (e.g. [{\"range\":\"Sheet1!A1:B2\",\"values\":[[\"a\",\"b\"]]}])"`
270+
ValueInput string `name:"input" help:"Value input option: RAW or USER_ENTERED" default:"USER_ENTERED"`
271+
IncludeValuesInResponse bool `name:"include-values-in-response" help:"Include updated values in the response"`
272+
ResponseValueRenderOption string `name:"response-render" help:"Response value render option: FORMATTED_VALUE, UNFORMATTED_VALUE, or FORMULA"`
273+
ResponseDateTimeRenderOption string `name:"response-date-time-render" help:"Response date/time render option: SERIAL_NUMBER or FORMATTED_STRING"`
274+
}
275+
276+
func (c *SheetsBatchUpdateCmd) Run(ctx context.Context, flags *RootFlags) error {
277+
u := ui.FromContext(ctx)
278+
279+
spreadsheetID := normalizeGoogleID(strings.TrimSpace(c.SpreadsheetID))
280+
if spreadsheetID == "" {
281+
return usage("empty spreadsheetId")
282+
}
283+
284+
data, err := parseSheetsBatchUpdateData(c.DataJSON)
285+
if err != nil {
286+
return err
287+
}
288+
289+
valueInputOption := strings.TrimSpace(c.ValueInput)
290+
if valueInputOption == "" {
291+
valueInputOption = sheetsDefaultValueInputOption
292+
}
293+
req := &sheets.BatchUpdateValuesRequest{
294+
Data: data,
295+
ValueInputOption: valueInputOption,
296+
IncludeValuesInResponse: c.IncludeValuesInResponse,
297+
}
298+
if strings.TrimSpace(c.ResponseValueRenderOption) != "" {
299+
req.ResponseValueRenderOption = strings.TrimSpace(c.ResponseValueRenderOption)
300+
}
301+
if strings.TrimSpace(c.ResponseDateTimeRenderOption) != "" {
302+
req.ResponseDateTimeRenderOption = strings.TrimSpace(c.ResponseDateTimeRenderOption)
303+
}
304+
305+
if dryRunErr := dryRunExit(ctx, flags, "sheets.batch-update", map[string]any{
306+
"spreadsheet_id": spreadsheetID,
307+
"value_input_option": req.ValueInputOption,
308+
"include_values_in_response": req.IncludeValuesInResponse,
309+
"response_value_render_option": req.ResponseValueRenderOption,
310+
"response_date_time_render_option": req.ResponseDateTimeRenderOption,
311+
"data": req.Data,
312+
}); dryRunErr != nil {
313+
return dryRunErr
314+
}
315+
316+
account, err := requireAccount(flags)
317+
if err != nil {
318+
return err
319+
}
320+
321+
svc, err := newSheetsService(ctx, account)
322+
if err != nil {
323+
return err
324+
}
325+
326+
resp, err := svc.Spreadsheets.Values.BatchUpdate(spreadsheetID, req).Do()
327+
if err != nil {
328+
return err
329+
}
330+
331+
if outfmt.IsJSON(ctx) {
332+
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
333+
"spreadsheetId": resp.SpreadsheetId,
334+
"totalUpdatedRows": resp.TotalUpdatedRows,
335+
"totalUpdatedColumns": resp.TotalUpdatedColumns,
336+
"totalUpdatedCells": resp.TotalUpdatedCells,
337+
"totalUpdatedSheets": resp.TotalUpdatedSheets,
338+
"responses": resp.Responses,
339+
})
340+
}
341+
342+
u.Out().Linef("Updated %d cells across %d ranges in %s", resp.TotalUpdatedCells, len(resp.Responses), spreadsheetID)
343+
return nil
344+
}
345+
346+
func parseSheetsBatchUpdateData(dataJSON string) ([]*sheets.ValueRange, error) {
347+
if strings.TrimSpace(dataJSON) == "" {
348+
return nil, usage("empty data-json")
349+
}
350+
b, err := resolveInlineOrFileBytes(dataJSON)
351+
if err != nil {
352+
return nil, fmt.Errorf("read --data-json: %w", err)
353+
}
354+
var data []*sheets.ValueRange
355+
if unmarshalErr := json.Unmarshal(b, &data); unmarshalErr != nil {
356+
return nil, fmt.Errorf("invalid JSON data: %w", unmarshalErr)
357+
}
358+
if len(data) == 0 {
359+
return nil, usage("--data-json must contain at least one value range")
360+
}
361+
for i, vr := range data {
362+
if vr == nil {
363+
return nil, usagef("--data-json range %d is null", i)
364+
}
365+
vr.Range = cleanRange(vr.Range)
366+
if strings.TrimSpace(vr.Range) == "" {
367+
return nil, usagef("--data-json range %d has empty range", i)
368+
}
369+
if len(vr.Values) == 0 {
370+
return nil, usagef("--data-json range %d has empty values", i)
371+
}
372+
}
373+
return data, nil
374+
}
375+
266376
type SheetsAppendCmd struct {
267377
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
268378
Range string `arg:"" name:"range" help:"Range (A1 notation or named range name; e.g. Sheet1!A:C or MyNamedRange)"`

0 commit comments

Comments
 (0)