feat(trail): surface trail web URL in create/show · Entire

feat(trail): surface trail web URL in create/show

7af26cb→main·

toothbrush·3w ago·3 files·+76 added/-6 removed

trail create now prints the trail's browser URL, and both create and show prefer the canonical url the server returns over a locally constructed one — so the CLI tracks any route change without needing a lockstep update. Local construction remains only as a fallback for older servers that omit the field.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

df9e84f74c5eView transcript

Changes

3

20 unmodified lines

21
22
23
24
25
24
25
26
27
28
29
30
31
32

20 unmodified lines

// TrailResource represents a single trail from the API.
type TrailResource struct {
    ID              string           `json:"id,omitempty"`
    Number          int              `json:"number,omitempty"`
    // URL is the canonical browser URL for the trail. The server provides it so
    // clients need not reconstruct the route; it may be empty against older
    // servers that predate the field.
    URL             string           `json:"url,omitempty"`
    Branch          string           `json:"branch"`
    Base            string           `json:"base"`
    Title           string           `json:"title"`
}

Mcmd/entire/cli/api/trail_types.go+6/-2

132 unmodified lines

133
134
135
136
136
137
138
139
140
141
142
143
143
144
145
94 unmodified lines

240
241
242
244
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
541 unmodified lines

812
813
814
792
815
816
817
818

132 unmodified lines

// the core metadata already came from the list, so a detail failure
        // falls back to the list body with a warning rather than failing.
        m := found.ToMetadata()
        webURL := ""
        webURL := trailDisplayURL(*found, forge, owner, repo)
        // Seed the description from the list body so a failed (or skipped)
        // detail fetch still shows something; a successful detail fetch
        // supersedes it with the richer body_document text below.
        bodyText := found.Body
        descriptionLoaded := strings.TrimSpace(found.Body) != ""
        if found.Number > 0 {
            webURL = trailWebURL(api.BaseURL(), forge, owner, repo, found.Number)
            if bt, derr := fetchTrailDescription(ctx, client, forge, owner, repo, found.Number); derr == nil {
                // A successful fetch means we authoritatively consulted the
                // description, but it only supersedes the seeded list body when
                94 unmodified lines

}
        }
        return ""
    }

// trailWebURL builds the browser URL for a trail:
// printCreatedTrail reports a newly created trail, including its browser URL
// (the same URL `trail show` surfaces) when one is available.
func printCreatedTrail(w io.Writer, t api.TrailResource, forge, owner, repo string) {
    fmt.Fprintf(w, "Created trail %q for branch %s (ID: %s)\n", t.Title, t.Branch, t.ID)
    if url := trailDisplayURL(t, forge, owner, repo); url != "" {
        fmt.Fprintf(w, "  URL: %s\n", url)
    }
}

// trailDisplayURL returns the trail's browser URL. It prefers the canonical URL
// the server now returns, so the CLI tracks any route change without being
// updated in lockstep, and falls back to a locally constructed URL only for
// older servers that omit the field.
func trailDisplayURL(t api.TrailResource, forge, owner, repo string) string {
    if strings.TrimSpace(t.URL) != "" {
        return t.URL
    }
    if t.Number > 0 {
        return trailWebURL(api.BaseURL(), forge, owner, repo, t.Number)
    }
    return ""
}

// trailWebURL builds a fallback browser URL for a trail used only when the
// server does not supply one (older servers):
// <web-origin>/<forge>/<owner>/<repo>/trails/<number>. In production the web app
// is served from the same origin as the data API, so the API base URL doubles
// as the web origin. A split local-dev setup (API and frontend on different
541 unmodified lines

return fmt.Errorf("failed to decode create response: %w", err)
}

fmt.Fprintf(w, "Created trail %q for branch %s (ID: %s)\n", createResp.Trail.Title, createResp.Trail.Branch, createResp.Trail.ID)
printCreatedTrail(w, createResp.Trail, forge, owner, repoName)

if needsCreation && currentBranch != branch {
    shouldCheckout := checkout
}
}

Mcmd/entire/cli/trail_cmd.go+27/-4

301 unmodified lines

302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350

301 unmodified lines

}

func TestPrintCreatedTrail(t *testing.T) {

t.Parallel()

// The server-provided URL is used verbatim. var out bytes.Buffer printCreatedTrail(&out, api.TrailResource{Title: "Fix it", Branch: "feat/x", ID: "abc123", Number: 575, URL: "https://entire.io/gh/acme/repo/trails/575/fix-it"}, "gh", "acme", "repo") text := out.String() if !strings.Contains(text, Created trail "Fix it" for branch feat/x (ID: abc123)) {

t.Fatalf("missing create summary line, got:\n%s", text) } if !strings.Contains(text, "URL: https://entire.io/gh/acme/repo/trails/575/fix-it") {

t.Fatalf("expected the server-provided URL, got:\n%s", text) }

// Without a number, omit the URL line. out.Reset() printCreatedTrail(&out, api.TrailResource{Title: "No num", Branch: "feat/y", ID: "def456"}, "gh", "acme", "repo") if text := out.String(); strings.Contains(text, "URL:") {

t.Fatalf("expected URL omitted when number and URL are absent, got:\n%s", text) } }

func TestTrailDisplayURL(t *testing.T) {

t.Parallel()

// Server URL wins, even when a number is present. got := trailDisplayURL(api.TrailResource{Number: 5, URL: "https://server/url"}, "gh", "acme", "repo") if got != "https://server/url" {

t.Fatalf("expected server URL, got %q", got) }

// Falls back to a constructed URL for older servers that omit it. got = trailDisplayURL(api.TrailResource{Number: 5}, "gh", "acme", "repo") if !strings.HasSuffix(got, "/gh/acme/repo/trails/5") {

t.Fatalf("expected constructed fallback URL, got %q", got) }

// Nothing to show when neither is available. if got := trailDisplayURL(api.TrailResource{}, "gh", "acme", "repo"); got != "" {

t.Fatalf("expected empty URL, got %q", got) } }

func TestTrailDescriptionForDisplay(t *testing.T) {

t.Parallel() if got := trailDescriptionForDisplay("the body", true); got != "the body" {

t.Fatalf("unexpected description output, got: %v", got) }}