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
5 changes: 5 additions & 0 deletions .changeset/docs-revisions-helper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": minor
---

Add `gws docs +revisions` helper to list revision history for a Google Docs document
72 changes: 72 additions & 0 deletions skills/gws-docs-revisions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: gws-docs-revisions
version: 1.0.0
description: "Google Docs: List revision history for a document."
metadata:
openclaw:
category: "productivity"
requires:
bins: ["gws"]
cliHelp: "gws docs +revisions --help"
---

# docs +revisions

> **PREREQUISITE:** Read `../gws-shared/SKILL.md` for auth, global flags, and security rules. If missing, run `gws generate-skills` to create it.

List revision history for a Google Docs document.

## Usage

```bash
gws docs +revisions --document <ID> [--limit <N>]
```

## Flags

| Flag | Required | Default | Description |
|------|----------|---------|-------------|
| `--document` | ✓ | — | Document ID (from the URL) |
| `--limit` | — | 20 | Maximum number of revisions to return (1–1000) |

## Examples

```bash
# Show last 20 revisions (default)
gws docs +revisions --document 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms

# Show last 5 revisions
gws docs +revisions --document 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms --limit 5
```

## Output

Each revision includes:

| Field | Description |
|-------|-------------|
| `id` | Revision ID |
| `modifiedTime` | When this revision was created |
| `lastModifyingUser.displayName` | Who made the change |
| `keepForever` | Whether this revision is pinned (won't be auto-deleted) |
| `size` | Size of the revision in bytes |

## Limitations

> [!IMPORTANT]
> **Content is not available.** The Google Drive API returns revision *metadata* only for
> native Google Docs files. The actual text content of past revisions cannot be retrieved
> via API — only the current content is accessible via `gws docs documents get`.
>
> To read a specific revision's content, open the document in Google Docs and use
> **File → Version history → See version history**.

## Scope

This command uses the `drive.readonly` OAuth scope. No write access is required.

## See Also

- [gws-shared](../gws-shared/SKILL.md) — Global flags and auth
- [gws-docs](../gws-docs/SKILL.md) — All Google Docs commands
- [gws-docs-write](../gws-docs-write/SKILL.md) — Append text to a document
1 change: 1 addition & 0 deletions skills/gws-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ gws docs <resource> <method> [flags]
| Command | Description |
|---------|-------------|
| [`+write`](../gws-docs-write/SKILL.md) | Append text to a document |
| [`+revisions`](../gws-docs-revisions/SKILL.md) | List revision history for a document |

## API Resources

Expand Down
44 changes: 44 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,50 @@ impl GwsError {
}
}

/// Build a [`GwsError::Api`] by parsing a Google API error response body.
///
/// Extracts `code`, `message`, `reason`, and (for `accessNotConfigured` errors) the
/// GCP console URL to enable the API. Falls back to the raw body when the
/// response is not well-formed Google JSON.
pub fn from_api_response(status: u16, body: &str) -> Self {
let err_json: Option<serde_json::Value> = serde_json::from_str(body).ok();
let err_obj = err_json.as_ref().and_then(|v| v.get("error"));
let code = err_obj
.and_then(|e| e.get("code"))
.and_then(|c| c.as_u64())
.map(|c| c as u16)
.unwrap_or(status);
let message = err_obj
.and_then(|e| e.get("message"))
.and_then(|m| m.as_str())
.unwrap_or(body)
.to_string();
let reason = err_obj
.and_then(|e| e.get("errors"))
.and_then(|e| e.as_array())
.and_then(|arr| arr.first())
.and_then(|e| e.get("reason"))
.and_then(|r| r.as_str())
.or_else(|| {
err_obj
.and_then(|e| e.get("reason"))
.and_then(|r| r.as_str())
})
.unwrap_or("unknown")
.to_string();
let enable_url = if reason == "accessNotConfigured" {
crate::executor::extract_enable_url(&message)
} else {
None
};
GwsError::Api {
code,
message,
reason,
enable_url,
}
}
Comment on lines +99 to +141
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This new function from_api_response is a good addition for parsing API errors. However, its logic is nearly identical to the existing executor::handle_error_response function. This code duplication can become a maintenance issue, as changes to error parsing logic would need to be applied in two places, potentially leading to inconsistencies. To improve maintainability, please consider refactoring executor::handle_error_response to use this new GwsError::from_api_response function. This would centralize the API error parsing logic in one place.

Comment on lines +104 to +141
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This new function from_api_response is a good abstraction for parsing Google API error responses. However, it duplicates logic that already exists in src/executor.rs within the handle_error_response function.

To improve maintainability and ensure consistent error handling, handle_error_response should be refactored to use this new, centralized function. This will prevent bugs from being fixed in one place but not the other and make future updates easier.

Since executor.rs is not part of this PR, I recommend creating a high-priority follow-up task to address this technical debt.


pub fn to_json(&self) -> serde_json::Value {
match self {
GwsError::Api {
Expand Down
Loading
Loading