Skip to content

Commit c2e5499

Browse files
committed
more improvements
1 parent bd11777 commit c2e5499

11 files changed

Lines changed: 96 additions & 37 deletions

File tree

DESIGN.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ revmap is a read-only CLI tool that queries the Snap Store's dashboard API to di
1010

1111
```
1212
revmap/
13-
main.go Entry point; embeds README, sets version via ldflags
13+
main.go Entry point; embeds README and DESIGN, sets version via ldflags
1414
cache-snaps.json Configuration: list of snaps to pre-cache
1515
cache/ Generated cache files (gitignored)
1616
demo.sh Interactive demo script (invoked by demo command)
1717
test.sh Unified test runner (--unit, --static, --all)
18+
version.sh Single source of truth for version string
1819
cmd/
1920
root.go Root Cobra command, group registration, command ordering
2021
version.go Version resolution (ldflags or VCS fallback)
@@ -23,28 +24,26 @@ revmap/
2324
whoami.go Account information display
2425
list.go Revision listing with filters and table output
2526
show.go Single revision detail view
26-
cache.go Cache-build subcommand (pre-build cache generation)
27+
helpers.go Shared utilities (cache fallback error detection)
2728
design.go Embedded DESIGN display (rendered via glamour)
2829
readme.go Embedded README display (rendered via glamour)
2930
demo.go Demo subcommand (runs demo.sh)
30-
design.go Embedded DESIGN display (rendered via glamour)
31-
readme.go Embedded README display (rendered via glamour)
32-
list_test.go Tests for list logic
33-
show_test.go Tests for show logic
34-
version_test.go Tests for version logic
31+
list_test.go Tests for list logic
32+
show_test.go Tests for show logic
33+
version_test.go Tests for version logic
3534
cmd/cache-build/
3635
main.go Standalone cache-build binary (separate main package)
3736
store/
3837
constants.go API URLs and app-wide constants
3938
auth.go Macaroon serialization, SSO discharge, login flow
4039
credentials.go File-based credential storage with env var override
4140
account.go Account info retrieval (whoami endpoint)
42-
client.go Authenticated HTTP client with auto-refresh
41+
client.go Authenticated HTTP client with connection pooling and auto-refresh
4342
revisions.go Store API calls (revisions, releases with pagination)
4443
cache.go Cache data structures, gzip read/write, file lookup
4544
auth_test.go Tests for macaroon serialization and caveat extraction
4645
credentials_test.go Tests for credential storage
47-
client_test.go Tests for refresh detection
46+
client_test.go Tests for client transport and refresh detection
4847
```
4948

5049
## Command Groups
@@ -55,9 +54,7 @@ Commands are organized into three groups displayed in `--help` output. Sorting i
5554
- **Query:** list, show
5655
- **Learn:** readme, design, demo
5756

58-
## Authentication
59-
60-
### Version
57+
## Version
6158

6259
The project produces two binaries (`revmap` and `cache-build`), both receiving the same version via ldflags at build time.
6360

@@ -67,7 +64,7 @@ The project produces two binaries (`revmap` and `cache-build`), both receiving t
6764

6865
Cobra's built-in `Version` field provides the `--version` flag automatically.
6966

70-
### Macaroon Scheme
67+
## Authentication
7168

7269
The Snap Store uses a two-macaroon authentication model:
7370

@@ -188,7 +185,7 @@ A standalone binary (`cmd/cache-build/main.go`) that fetches the complete revisi
188185

189186
Built by `make build` alongside the main binary, with the same version injected via ldflags. Supports `-version` to print its version.
190187

191-
**Performance:** The HTTP client is created via `NewClientWithWorkers(n)` which configures a transport with `MaxIdleConnsPerHost` matching the worker count, ensuring TCP/TLS connections are reused across concurrent requests rather than being re-established.
188+
**Performance:** The HTTP client is created via `NewClientWithWorkers(n)` which configures a transport with `MaxIdleConnsPerHost` set to `n + 10`, ensuring TCP/TLS connections are reused across concurrent requests rather than being re-established.
192189

193190
**Authentication:** If credentials already exist (user ran `revmap login` or `SNAPCRAFT_STORE_CREDENTIALS` is set), they are used directly. Otherwise, `cache-build` checks for `REVMAP_EMAIL` and `REVMAP_PASSWORD` environment variables and performs a non-interactive login via `store.Login(email, password, "")`. The OTP parameter is always empty — the account must not have two-factor authentication enabled. A 2FA-enabled account will return `ErrTwoFactorRequired`, surfaced as `"automatic login failed: two-factor authentication required"`.
194191

@@ -267,6 +264,7 @@ All user-facing messages follow consistent conventions:
267264
| Package | Purpose |
268265
|---|---|
269266
| `github.com/spf13/cobra` | CLI framework |
267+
| `github.com/charmbracelet/glamour` | Terminal markdown rendering (readme/design commands) |
270268
| `golang.org/x/term` | Secure password input (no echo) |
271269
| `gopkg.in/macaroon.v1` | Macaroon creation, serialization, binding (matches snapd) |
272270

README.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,24 @@ The demo uses the snapd snap as an example and walks through
172172
list/show commands with various filters, including cache
173173
fallback.
174174

175+
### readme / design
176+
177+
Display documentation directly in the terminal, rendered with
178+
styled markdown (headers, code blocks, syntax highlighting):
179+
180+
revmap readme
181+
revmap design
182+
183+
The `design` command is particularly useful as context for
184+
coding agents before making contributions.
185+
175186
### cache-build
176187

177-
Build the offline cache for all snaps in `cache-snaps.json`:
188+
`cache-build` is a standalone binary (not a revmap subcommand) that
189+
builds the offline cache for all snaps in `cache-snaps.json`:
178190

179-
revmap cache-build
180-
revmap cache-build --workers 50
191+
./cache-build
192+
./cache-build -workers 50
181193

182194
Requires authentication. See the Offline Cache section below
183195
for the full workflow.
@@ -223,7 +235,7 @@ snaps.
223235

224236
Options:
225237

226-
revmap cache-build --workers 50
238+
./cache-build -workers 50
227239

228240
3. Build the snap (cache is bundled automatically):
229241

@@ -240,7 +252,7 @@ set environment variables instead:
240252

241253
export REVMAP_EMAIL="user@example.com"
242254
export REVMAP_PASSWORD="secret"
243-
revmap cache-build
255+
./cache-build
244256

245257
The account **must not** have two-factor authentication (2FA)
246258
enabled. A 2FA-enabled account will fail with "two-factor
@@ -297,8 +309,8 @@ latest git tag. Check with:
297309

298310
./cache-build -version
299311

300-
The `cache` target depends on `build` and runs
301-
`revmap cache-build`. You must be logged in first:
312+
The `cache` target runs `go run ./cmd/cache-build`.
313+
You must be logged in first:
302314

303315
revmap login
304316
make cache

cmd/cache-build/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
package main
55

66
import (
7+
"errors"
78
"flag"
89
"fmt"
910
"os"
1011
"strconv"
11-
"strings"
1212
"sync"
1313
"time"
1414

@@ -133,7 +133,8 @@ func buildCacheForSnap(client *store.Client, snapName string, workers int) error
133133
if err != nil {
134134
// Skip 404s — some revisions in the releases list
135135
// may have been deleted from the revision endpoint.
136-
if strings.Contains(err.Error(), "status 404") {
136+
var storeErr *store.StoreError
137+
if errors.As(err, &storeErr) && storeErr.StatusCode == 404 {
137138
mu.Lock()
138139
if (idx+1)%100 == 0 {
139140
fmt.Printf(" %d/%d details fetched\n", idx+1, len(releases.Revisions))

cmd/demo.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ using the snapd snap as an example. Requires authentication.
2424
The demo walks through various list and show commands,
2525
pausing between each for review. Use --no-pause to run
2626
without pauses.`,
27+
Args: cobra.NoArgs,
2728
RunE: func(cmd *cobra.Command, args []string) error {
2829
script := findDemoScript()
2930
if script == "" {

cmd/helpers.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package cmd
22

3-
import "strings"
3+
import (
4+
"errors"
5+
6+
"github.com/ernestl/revmap/store"
7+
)
48

59
// isCacheFallbackErr returns true if the error indicates a permission
610
// or access issue (401, 403, 404) where falling back to cache is appropriate.
711
func isCacheFallbackErr(err error) bool {
8-
if err == nil {
9-
return false
12+
var storeErr *store.StoreError
13+
if errors.As(err, &storeErr) {
14+
switch storeErr.StatusCode {
15+
case 401, 403, 404:
16+
return true
17+
}
1018
}
11-
msg := err.Error()
12-
return strings.Contains(msg, "status 401") ||
13-
strings.Contains(msg, "status 403") ||
14-
strings.Contains(msg, "status 404")
19+
return false
1520
}

cmd/list.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,14 @@ func init() {
103103
listCmd.Flags().StringVarP(&columns, "columns", "c", defaultColumns, "columns to display (comma-separated)")
104104
}
105105

106-
// columnNames returns the sorted list of available column names.
106+
// columnNames returns the available column names in display order.
107107
func columnNames() string {
108-
return "revision, version, arch, status, created, confinement, base, size"
108+
names := make([]string, 0, len(allColumns))
109+
for name := range allColumns {
110+
names = append(names, name)
111+
}
112+
sort.Strings(names)
113+
return strings.Join(names, ", ")
109114
}
110115

111116
var listCmd = &cobra.Command{
@@ -121,6 +126,9 @@ By default only the last 90 days are shown. Use --all to fetch
121126
complete history, or --limit/-n to fetch up to a specific number
122127
of revisions across all pages.
123128
129+
Available columns: arch, base, confinement, created, revision, size, status, version.
130+
Build types: release, fips (comma-separated).
131+
124132
Examples:
125133
revmap list snapd
126134
revmap list snapd --since 7d -a amd64
@@ -242,6 +250,8 @@ func listFromCache(snapName, reason string) error {
242250

243251
// applyCacheTimeWindow filters cached revisions by the time window
244252
// and limit flags (same logic as FetchOptions but applied locally).
253+
// Revisions are sorted newest-first before applying limits so that
254+
// --limit returns the N most recent revisions.
245255
func applyCacheTimeWindow(revisions []store.RevisionEntry) []store.RevisionEntry {
246256
opts, err := parseTimeWindow(since, until, limit, fetchAll)
247257
if err != nil {
@@ -250,6 +260,11 @@ func applyCacheTimeWindow(revisions []store.RevisionEntry) []store.RevisionEntry
250260
return revisions
251261
}
252262

263+
// Sort newest-first before applying limits.
264+
sort.Slice(revisions, func(i, j int) bool {
265+
return revisions[i].Revision > revisions[j].Revision
266+
})
267+
253268
var result []store.RevisionEntry
254269
for _, rev := range revisions {
255270
if !opts.Since.IsZero() || !opts.Until.IsZero() {

cmd/login.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ with snapcraft export-login output to skip interactive login.`,
4949
return nil
5050
}
5151

52-
if store.CredentialsExist() {
52+
// Without --export, check if already logged in (file or env var).
53+
if loginExportFile == "" && store.CredentialsExist() {
5354
fmt.Println("You are already logged in. Run 'revmap logout' first to re-authenticate.")
5455
return nil
5556
}

cmd/readme.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ func SetReadme(content string) {
1717
var readmeCmd = &cobra.Command{
1818
Use: "readme",
1919
Short: "Display the full README documentation",
20-
Args: cobra.NoArgs,
20+
Long: `Display the full README rendered with terminal styling.
21+
Useful as a quick reference for available commands, flags,
22+
and workflows without leaving the terminal.`,
23+
Args: cobra.NoArgs,
2124
Run: func(cmd *cobra.Command, args []string) {
2225
out, err := glamour.Render(embeddedReadme, "auto")
2326
if err != nil {

cmd/show.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ Requires authentication. Run 'revmap login' first.
2525
If not authenticated or lacking permissions, cached data is
2626
used automatically when available.
2727
28+
Without --fields, the full JSON response is displayed. Use -f to
29+
select specific fields from the revision object (e.g. version,
30+
status, architectures, created_at, base, confinement, size).
31+
2832
Examples:
2933
revmap show snapd 17339
3034
revmap show snapd 17339 -f version,status,architectures`,

store/client.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
9292

9393
// needsRefresh checks whether a 401 response contains the
9494
// "macaroon-needs-refresh" error code from the store.
95+
// It reads and restores the response body so the caller can
96+
// still consume it if this returns false.
9597
func needsRefresh(resp *http.Response) bool {
9698
var body struct {
9799
ErrorList []struct {
@@ -103,8 +105,9 @@ func needsRefresh(resp *http.Response) bool {
103105
} `json:"error-list"`
104106
}
105107

106-
// Read and parse the body to check for the refresh error code.
108+
// Read the body, then restore it for the caller.
107109
raw, err := io.ReadAll(resp.Body)
110+
resp.Body = io.NopCloser(bytes.NewReader(raw))
108111
if err != nil {
109112
return false
110113
}

0 commit comments

Comments
 (0)